> Handling live secrets in AI coding sessions — from The Handover, the-handover.org/docs/live-secrets-in-ai-sessions > Authors: Leon Mallett (captivated.online) with Claude Code · Last confirmed working: 2026-08-07 > © Captivated Ltd — free to use in your own work, not to redistribute as a collection. the-handover.org/licence 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:** ```bash security add-generic-password -U -a "$USER" -s "" -w # -w with NO value => interactive, hidden prompt # -U => update if it already exists ``` Never pass the value inline as `-w ''`. That records it in shell history *and* exposes it in the process list. **2. The assistant reads it inline, without echoing:** ```bash curl -H "Authorization: Bearer $(security find-generic-password -s -w)" ... printf '%s' "$(security find-generic-password -s -w)" | 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:** ```bash security find-generic-password -s -w | wc -c # length only security find-generic-password -s -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 — `-` — 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.