Skip to content
The Handover

CodingGuides

Handling live secrets in AI coding sessions

Hard-Won

Getting 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

TaskDoDon’t
Store a secretinteractive prompt, no value on the command linepass the value inline
Use a secretcommand substitution straight into a pipeecho, assign-then-log, visible argument
Password-typed setupthe human runs itthe assistant runs it with the secret inline
Test secretsfake values in committed test configreal values only in an ignored file
Production secretspiped into the platform secret storecommitted to config or workflow YAML
Leaked in transcriptrotate or revoke, and say soassume it is probably fine
Before committingcheck status, grep the staged diffstage 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.