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 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.
envandprintenvwould dump the environment;keyrefuses them, and unwrapsenv -u A B=1 cmdso the command that really runs is the one checked. key getrefuses where output is recorded. SetKEY_RECORDED=1in an agent session or CI andkey getstops, pointing atkey with.- Names, not values.
key lslists names;key addreads 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,
keycannot stop it. - On macOS, storing passes the value to
securityas an argument, so it is briefly visible topsduringkey add. Running withkey withhas 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.