anthropics / anthropics/claude-code
[Configuration and Settings] Secrets that enter through the model's own tool calls fan out to every on-disk store, unredacted — and there is no way to find or scrub them
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 147k
- Forks
- 24k
- PR merge metrics
- PR metrics pending
Description
Preflight Checklist
- I have searched existing requests and this feature hasn't been requested yet
- This is a single feature request (not multiple features)
Problem Statement
Closest prior art: #50014 (redaction at the session-log writer — closed not-planned/stale, closing note invites a new issue referencing it). Complementary, not duplicates: #90301 (user → Claude input channel), #29910 (built-in store + cloud backends), #88380 (paste-time detection), #77084 (broker injection), #82796 (model ignores "never print", closed stale). This is a single request: redact-at-write + audit/scrub for the tool channel — tool_use inputs and tool_result outputs. It deliberately does not re-ask for a secret store, a masked prompt, or paste scanning; those have their own issues.
#90301 makes the case that the chat prompt is a leaky input channel. There is a second channel with the same properties and no open issue: the model's own tool calls. A secret reaches the transcript whenever Claude composes a command that embeds one, or reads one back from a command's stdout — and the second case is the normal way to use every cloud secret manager from a CLI.
Concrete incident (2026-09-20, macOS, Claude Code 2.1.238 → 2.1.278, auto mode):
- Months earlier, in an autonomous session, the model wrote a Bash call of the form
SOME_TOKEN=ghp_… yarn install— a real GitHub PAT it had obtained from the environment, inlined as an env assignment. The auto-mode classifier blocked the call. Correct decision, but the blockedtool_usewas still persisted verbatim, so the block itself turned a transient value into a durable record. - Later, a
/doctor-style health check aggregated denied tool calls across transcripts and printed the top denied commands — re-echoing the live token into a new transcript while "warning" about it. (#82796 documents this failure mode from the model's side; the point here is that nothing in the write path stops it.) - Rotation took most of an hour, mostly finding the copies. A name-only scan of
~/.claudefound the same value in five distinct kinds of store: the main session.jsonlfiles (5 sessions),subagents/agent-*.jsonl(2),~/.claude/history.jsonl(prompt history — which also held a second, unrelated PAT from a paste three months earlier),<session>/auto-mode-classifier-error.txt, and two SQLite databases belonging to a third-party memory plugin that ingests transcripts. #50014 listed paste-cache, file-history and debug logs on top of that. None of these are documented as a set anywhere.
Why the tool channel matters more than the prompt channel for infrastructure work — the "use a secret" command is the leak, on every platform:
| Platform | The natural command | What lands in tool_result |
|---|---|---|
| GCP | gcloud secrets versions access latest --secret=X |
the secret |
| AWS | aws secretsmanager get-secret-value --secret-id X |
JSON with SecretString |
| Azure | az keyvault secret show --vault-name V --name X |
JSON with value |
| Firebase | firebase functions:secrets:access X |
the secret |
| Kubernetes | kubectl get secret X -o yaml |
base64 of every key |
| GitHub | gh auth token; gh secret set … --body |
token in stdout / argv |
| 1Password | op read op://… |
the secret |
| local files | cat ~/.npmrc, cat .env, env, printenv |
every token on the machine |
Each of these is a legitimate, often required step to accomplish a task. Guidance ("never print secrets") is advisory and demonstrably loses (#82796, and the incident above). Prevention has to sit in the write path, where the product owns it.
Secondary, same root cause: when a Bash call is denied, permissions.allow rules minted from that call (by /fewer-permission-prompts-style tooling) can carry the literal argument values into settings.local.json — related to #5544.
Proposed Solution
Four pieces, all on the write side, none requiring a secret store:
-
Redact at the transcript-writer boundary for both directions of the tool channel. Before a
tool_useinput or atool_resultbody is persisted (main transcript, subagent transcripts, history, classifier/debug logs, the paste cache), run the precision-biased pattern set from #88380 (ghp_/gho_/ghs_/github_pat_,AKIA,sk-,xox[baprs]-,glpat-,AIza, JWT triples,Bearer …, PEM blocks,user:pass@in URLs,^[A-Z0-9_]*(SECRET|TOKEN|PASSWORD|KEY)[A-Z0-9_]*=\S+) and replace matches with a typed marker, e.g.[redacted:github-pat:…k9Qx2m]— keep a 4–6 char tail so the user can identify which token it was. Emit a one-line notice in the session (⚠ secret-shaped value redacted from tool output). The model still sees the value in-context for the current turn where the task needs it; only the on-disk record is sanitised. Opt-out per pattern for the "decode this JWT" case. -
A blocked call must not become a durable record. When the classifier or a deny rule refuses a
tool_use, persist the command shape (pattern-redacted, values elided) rather than the verbatim string — the refusal is evidence the content was sensitive. -
Document the store inventory, and give plugins/hook authors a contract. One page listing every path under
~/.claude(and per-session dirs) that can contain transcript-derived text, and a stated guarantee that what reaches hooks,/compact,--resume,/buguploads and third-party plugins is the redacted stream. -
claude secrets scan [--scrub](or a/doctorcheck): scan all stores from (3) for the pattern set, report per-file counts — never values — and, with--scrub, rewrite in place preserving JSONL validity, refusing to touch the live session file. This is the remediation half; today it is a hand-rolledgrep -rlE/sed -iloop that must itself be careful not to put the value into the transcript again.
Alternative Solutions
- Prompt-only detection (#88380) — necessary, but covers one of the two channels.
- A secret store with broker injection (#29910, #77084) — solves future usage of secrets the user has enrolled; does nothing for the
gcloud secrets versions accessoutput of a secret nobody enrolled, or for the model composing an inline env assignment. - Encrypt transcripts at rest (#50014 item 5) — reduces blast radius of disk access, but the value is still re-sent as context, summarised by
/compact, and readable by every plugin.
Priority
High - Significant impact on productivity
Feature Category
Configuration and settings
Use Case Example
An infra engineer asks Claude to fix a failing deploy. Claude runs gcloud secrets versions access latest --secret=NPM_TOKEN to compare it with what CI has, then gh secret set NPM_TOKEN --body …. Both calls succeed, the deploy is fixed, and the token now exists verbatim in the transcript, the history file, any subagent that ran the check, and the memory plugin's database — with no notice, no way to know, and no command to clean it up. With (1)+(2) the transcript holds [redacted:gcp-secret]; with (4) the engineer can audit the machine in one command.
Additional Context
No response
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by locating the writers for main and subagent .jsonl transcripts, history.jsonl, auto-mode-classifier-error.txt, and the paste cache, then trace how denied tool calls and tool_result output reach those stores. Review the existing pattern set from #88380 and the related write paths in #50014. Done means tool-channel values are redacted across all listed stores and a scan/scrub operation reports counts without recording secret values.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- cli, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100