Skip to content

Collect Secrets

Get a secret value from a human into a destination (a GitHub repo secret, a Cloudflare Worker secret, or a gitignored .dev.vars file) without leaking it anywhere along the way. Use the collocated collect-secrets.sh tool instead of hand-rolling a paste script per setup.

When to use

Any setup flow that needs a human-held secret to reach a destination: GitHub Actions auth tokens, R2 / GCS keys, Cloudflare Worker secrets, OAuth tokens, database URLs. The problem of handing a secret to automation safely recurs with the same shape every time, so solve it once here and call it from everywhere.

The obvious-but-wrong paths this replaces:

  • Pasting into the chat → the value lands in the transcript forever.
  • Letting the agent read ~/.config/gcloud/, .env, or shell history → credential-exploration, correctly blocked by the permission classifier.
  • gh secret set --body "$VAL" → the value goes on the command line, where it leaks into ps output and shell history. Always pipe over stdin instead.

The pattern

sh
# GitHub repo secret(s)
./skills/security/collect-secrets.sh \
  --destination gh --repo OWNER/REPO NAME [NAME...]

# Cloudflare Worker secret(s)
./skills/security/collect-secrets.sh \
  --destination wrangler [--env ENV] NAME [NAME...]

# Gitignored .dev.vars (the file is the intended on-disk destination)
./skills/security/collect-secrets.sh \
  --destination dev-vars [--file PATH] NAME [NAME...]

For each NAME the tool prompts once with hidden input (read -rs), reads the value from stdin, and pipes it straight to the destination. It then prints a length-only confirmation (set NAME (N chars)) so you can sanity-check a paste without revealing it.

Guarantees

  • Hidden input, one secret at a time; empty input re-prompts, end-of-input aborts.
  • Never echoed, never placed on a command line (so it stays out of ps and shell history), never written to disk beyond the intended destination.
  • stdin-piped to the destination: the value reaches gh / wrangler over stdin, never as an argument. The destination reads its own stdin from the internal pipe, so it never competes with the hidden read.
  • Length-only confirmation: the only feedback is the character count.
  • Idempotent and re-runnable: gh / wrangler overwrite the secret; dev-vars replaces the key in place (no duplicate lines). The .dev.vars file is written with mode 600, and the tool warns if it is not gitignored.

Pluggable destinations

  • gh: a GitHub repo secret, via gh secret set NAME --repo OWNER/REPO reading the value over stdin.
  • wrangler: a Cloudflare Worker secret, via wrangler secret put NAME (optionally --env ENV) reading the value over stdin.
  • dev-vars: a gitignored .dev.vars file, with NAME=value appended or replaced in place at mode 600.

Add a destination by writing one function that reads the value from stdin and must not place it on argv.