Skip to content

key: hand a secret to one command, and nowhere else

The problem

A script needs an API token. Every usual way to give it one leaks: a shell profile export hands it to every program; a .env file sits in plain text; --token "$(cat token)" puts it where ps can read it; echo $TOKEN to check it lands in scrollback, CI logs, and the transcript of any AI agent driving the shell. Once something else runs your commands, everything printed is kept.

The method

Keep the secret in the system keychain, and let the program that needs it fetch it itself, straight into one child's environment:

key with deploy=API_TOKEN -- ./deploy.sh

key looks up deploy, exports it as API_TOKEN and execs the command. The value never appears in the command line or on stdout. The shell, and whatever records it, see only the command.

Three guards make that hold up:

  • Commands that would print it are refused. env and printenv would dump the environment; key refuses them, and unwraps env -u A B=1 cmd so the command that really runs is the one checked.
  • key get refuses where output is recorded. Set KEY_RECORDED=1 in an agent session or CI and key get stops, pointing at key with.
  • Names, not values. key ls lists names; key add reads hidden input and reports only the length.

Sample

The heart of it, in POSIX sh:

case $(basename "$1") in
  env|printenv|set|export|declare)
    die "refusing: '$1' prints the environment, secret included" ;;
esac
v=$(lookup "$name") && [ -n "$v" ] || die "no key called '$name'"
export "$var=$v"; unset v
exec "$@"

lookup is security find-generic-password -w on macOS, secret-tool lookup on Linux, or a mode-600 file for CI and tests.

Limits

  • The child has the secret. If it prints its own environment, key cannot stop it.
  • On macOS, storing passes the value to security as an argument, so it is briefly visible to ps during key add. Running with key with has no such gap.
  • The refusal list guards against the obvious mistake; it is not a sandbox.

Code

methods/key/ in the methods repository: the script, a README and a test that runs on the file backend with no keychain.