Appearance
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 intopsoutput 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...]
# macOS Keychain item (service = NAME)
./skills/security/collect-secrets.sh \
--destination keychain [--account ACCOUNT] 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
psand shell history), never written to disk beyond the intended destination. - stdin-piped to the destination: the value reaches
gh/wrangler/securityover 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/wrangleroverwrite the secret andkeychainupdates the item in place (-U, so no duplicate entries);dev-varsreplaces the key in place (no duplicate lines). The.dev.varsfile is written with mode600, and the tool warns if it is not gitignored.
Pluggable destinations
gh: a GitHub repo secret, viagh secret set NAME --repo OWNER/REPOreading the value over stdin.wrangler: a Cloudflare Worker secret, viawrangler secret put NAME(optionally--env ENV) reading the value over stdin.dev-vars: a gitignored.dev.varsfile, withNAME=valueappended or replaced in place at mode600.keychain: a macOS Keychain generic-password item with serviceNAME, viasecurity add-generic-password -U.--accountoverrides the default${USER:-$(id -un)}; the fallback matters becauseUSERis unset under cron and launchd, and a secret stored under one account is invisible to a lookup using the other.
Add a destination by writing one function that reads the value from stdin and must not place it on argv.
The keychain destination shows what that can take. security has no stdin-value flag: -w with an argument would put the secret on argv, and -w with none prompts twice (value, then retype) reading both from stdin. So the function feeds the value in twice rather than passing -w "$value". A destination whose tool "has no stdin option" usually has one in disguise.
Keychain as a credential source
Storing a credential in the Keychain is only half a setup; something has to read it back. The pattern that works in every context (interactive shells, scripts, services, cron, and agents) is a small wrapper executable early on PATH that reads the item with security find-generic-password, exports it into only that tool's environment, and execs the real binary by absolute path. A shell function in ~/.bashrc looks equivalent and is not: it covers interactive bash only.
Two things to check when adopting it, both of which silently disable the wrapper rather than failing loudly:
- The wrapper's directory must come before the real binary's on
PATH, or the real binary shadows the wrapper and every call bypasses auth. - The wrapper must
execthe real binary by absolute path, or it re-enters itself.