Handling live secrets in AI coding sessions
Hard-WonGetting a credential from its store into its destination without it ever appearing as text the assistant emits.
- Authors
- Leon Mallett, Founder of Captivated Ltd with Claude Code
- Status
- Last confirmed working 7 August 2026 on security (macOS Keychain CLI) macOS 14+
- Written
- 7 August 2026
- Licence
- Handover-1.0
This covers the operator side of security in an AI-assisted session: real API tokens, signing keys, webhook secrets and passwords that pass through a live working session. It is the companion to supply-chain guidance, which covers the dependency side.
The threat model
A secret pasted into, or echoed by, a command the assistant runs is captured in the conversation transcript — which is stored, may be reviewed, and is outside your control. Secrets also leak through shell history, through process lists (command-line arguments are visible to other processes), and through accidental commits.
None of this can be undone. An assistant cannot un-emit a secret, and a transcript cannot be selectively forgotten. The only remedy after a leak is to rotate the credential.
So the whole game is: get the secret from its source into its destination without it ever appearing as text that the assistant emits or a command records.
The core pattern: a secret store as the courier
Store the secret once, then reference it by name. The value substitutes into a command at runtime and is never typed, echoed, or logged. Examples below use the macOS Keychain; the pattern applies to any secret store with a CLI that can write to stdout.
1. The human stashes it, prompted, so it never reaches a command line:
security add-generic-password -U -a "$USER" -s "<name>" -w
# -w with NO value => interactive, hidden prompt
# -U => update if it already exists
Never pass the value inline as -w '<the-secret>'. That records it in shell
history and exposes it in the process list.
2. The assistant reads it inline, without echoing:
curl -H "Authorization: Bearer $(security find-generic-password -s <name> -w)" ...
printf '%s' "$(security find-generic-password -s <name> -w)" | <tool> secret put NAME
The substituted value goes straight into the process. It is not printed to the
transcript. Do not echo it, do not assign it to a variable that later gets
logged, and do not pass it as a visible argument.
3. Confirm presence without revealing the value:
security find-generic-password -s <name> -w | wc -c # length only
security find-generic-password -s <name> -w | cut -c1-6 # non-secret prefix
A prefix such as sk_ or whsec_ is enough to confirm you have the right kind
of credential without disclosing it.
Name Keychain items predictably — <project>-<purpose> — so they are greppable
and scoped per project.
Rules of thumb
-
The assistant must never emit a real secret as text, and must never run a command that echoes one. Reading a value into a pipe via command substitution is fine. Printing it is not.
-
The human runs any step that requires a password to be typed. Anything that prompts interactively — credential-store setup, notarisation credential storage, an authenticated prompt-based login — should be run by the person, not the assistant, so the secret bypasses the transcript entirely. The assistant supplies the command; the human executes it.
-
If a secret does land in the transcript, treat it as burned. Rotate or revoke it, and say so plainly. A low-value sandbox token may be acceptable to use and then revoke; a production secret pasted into a conversation should be rotated immediately, not rationalised.
-
Secret-bearing files stay ignored by git; test values live in committed config. Environment files, key files and certificate bundles belong in
.gitignore. But the fake values a test suite needs belong in committed test configuration, not the ignored file — otherwise tests pass locally and fail in CI, where the file does not exist. Prove it by moving the ignored file aside and running the suite: it must still pass. -
Scan before committing. Check status before staging everything, so stray files are not swept in, and grep the staged diff for credential-shaped strings —
sk_,whsec_,-----BEGIN,Bearer— before you commit. -
Least privilege at the source. Mint scoped tokens rather than root ones: a single-product token over an account-admin token, a scoped cloud key over a personal login. Smaller blast radius if it ever leaks. Verify the scope is real by checking that the intended calls succeed and the withheld ones are refused.
-
Secrets go into platform secret stores, not repository config. Pipe them from the local store into the platform’s own secret storage. Never commit them to configuration files or workflow YAML. Non-secret identifiers — product ids, policy ids, redirect URIs — are fine to commit.
-
Prefer short-lived credentials over long-lived ones wherever the platform offers them, especially in CI.
Quick reference
| Task | Do | Don’t |
|---|---|---|
| Store a secret | interactive prompt, no value on the command line | pass the value inline |
| Use a secret | command substitution straight into a pipe | echo, assign-then-log, visible argument |
| Password-typed setup | the human runs it | the assistant runs it with the secret inline |
| Test secrets | fake values in committed test config | real values only in an ignored file |
| Production secrets | piped into the platform secret store | committed to config or workflow YAML |
| Leaked in transcript | rotate or revoke, and say so | assume it is probably fine |
| Before committing | check status, grep the staged diff | stage everything blind |
Why the assistant should refuse
An assistant asked to print a secret “just to check it worked” is being asked to create a permanent record of that secret in exchange for a moment of convenience. There is a non-destructive way to answer the same question — length and prefix, as above — so the reasonable response is to offer that instead.
This is not caution for its own sake. It is the recognition that the cost is asymmetric: verifying by length costs nothing, and verifying by disclosure costs a rotation and whatever happened before someone noticed.