Repository navigation
Description
Summary
Claude Code will read and echo the contents of secret-bearing files (.env, .dev.vars, credential files) into the conversation transcript, even when the user's CLAUDE.md contains explicit prohibitions against doing so. The model's safety reflex fires on output it has already produced, not on commands it is about to run, so the violation is detected only after the secret has already been written to chat history — at which point rotation is the only remedy.
Reproduction
- User has a global
CLAUDE.mdcontaining rules like:NEVER output secret values into chat. NEVER read
.envfiles unless strictly necessary, and never reproduce any values in your response. - User asks Claude Code to help populate a
.dev.varsfile with API tokens. - User accidentally pastes a malformed value (e.g. a
curlexample command) instead of a bare token. - Claude Code attempts to diagnose by running:
grep -n curl .dev.vars grep -nprints the full matching line, including the real API token, into the tool result — which is rendered into the conversation transcript and persisted in history.- Claude Code then notices the violation, apologizes, and instructs the user to rotate the exposed credential.
This is reproducible with any inspection command that can echo file contents: Read, cat, head, tail, grep without -c, sed/awk patterns that print full lines, etc. The model knows the rules but does not pre-screen its own commands against them.
Why the existing safeguards don't work
CLAUDE.mdinstructions are advisory — they shape intent but don't block tool calls at execution time.- The model's safety check runs on generated output it can see, not on the side effects of tool calls it's about to issue.
- "Operational reflexes" (e.g. defaulting to
grep -nfor line numbers) override the rule because the rule is stored as guidance, not as a command-level filter. - The remediation flow ("apologize and ask the user to rotate") is fired after the credential is already in conversation history, server logs, and any downstream telemetry. By then the damage is done.
Proposed fix
A defense-in-depth approach at the harness level, not the prompt level:
- Path-based pre-execution filter. When a Bash/Read/Grep tool call targets a path matching a known secret pattern (
.env*,.dev.vars*,*.pem,*.key,credentials*.json,*secret*,*token*), require the command to be on an allowlist of value-stripping operations (wc -l,grep -c,stat, length-onlyawkpatterns, blindEditwith literalold_string). Block everything else by default. - Output-side scrubber. Before tool results are appended to the conversation, run a regex pass over their stdout looking for high-entropy strings, known token prefixes (
sk-,sk-ant-,cfut_,ghp_,xox[bp]-, AWS access keys, JWT shapes, etc.) and redact them with a<REDACTED:reason>placeholder. The model should see the redaction marker, not the value. - First-class "this file contains secrets" annotation. When the model has reason to believe a file contains secrets (filename, prior context, user statement), surface that as a structured flag in its working state, not just as a soft instruction in the prompt.
- Make the pre-execution filter overridable but loud. A user can explicitly opt-out for a one-shot command, but the override should be per-command, logged, and never the default.
The key principle: the model should not be trusted to police its own tool calls against secrets. The harness should enforce it.
User impact
This is not a hypothetical. Real reproduction in a real session today:
- A Cloudflare API token was written to chat history while diagnosing a malformed
.dev.varsfile. - The token had to be rotated immediately, requiring re-issuance, re-pasting into the file, and re-verification.
- The user had to interrupt their actual work (deploying a Cloudflare Worker) to deal with the cleanup.
- The exposed value exists in conversation history, transmission logs, and any retained telemetry between the user's machine and Anthropic's servers — beyond the user's direct control to scrub.
For developers using Claude Code on client work, the consequences scale fast: a leaked production credential during a paid engagement is a trust-and-reputation event, not just a chore. The implicit cost of this class of bug — minutes per incident, multiplied across the developer base, multiplied by the percentage of incidents that involve real production secrets — is substantial. Anthropic should consider whether affected users are owed credit for time lost to cleanup caused by the agent's own failure to honor its documented safety rules, in addition to fixing the underlying bug.
Environment
- Claude Code CLI on Windows 11 (MINGW64 / Git Bash)
- Model: Claude Opus 4.6 (1M context)
- Project has a global
CLAUDE.mdwith explicit secret-handling rules - User confirmed reproducibility immediately after the incident
Activity
github-actions commentedon Apr 7, 2026
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
yurukusa commentedon Apr 7, 2026
You're absolutely right that CLAUDE.md instructions are advisory — they shape model intent but can't block tool execution. The fix is exactly what you described: harness-level enforcement via PreToolUse hooks.
Here's a working set of hooks that blocks secret file access across all tool types. Add this to your
~/.claude/settings.json:{ "hooks": { "PreToolUse": [ { "matcher": "Read", "hooks": [{ "type": "command", "command": "bash -c 'INPUT=$(cat); FILE=$(echo \"$INPUT\" | jq -r \".tool_input.file_path // empty\"); [ -z \"$FILE\" ] && exit 0; if echo \"$FILE\" | grep -qiE \"\\.(env|env\\.[a-z]+|dev\\.vars|pem|key)$|credentials|secrets\"; then echo \"BLOCKED: Reading secret file: $FILE\" >&2; echo \"If you need specific values, ask the user to provide them directly.\" >&2; exit 2; fi'" }] }, { "matcher": "Bash", "hooks": [{ "type": "command", "command": "bash -c 'INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r \".tool_input.command // empty\"); [ -z \"$CMD\" ] && exit 0; if echo \"$CMD\" | grep -qE \"(cat|head|tail|less|more|grep|sed|awk|sort|cut)\\s+.*\\.(env|dev\\.vars|pem|key)\\b\"; then echo \"BLOCKED: Command reads secret file.\" >&2; echo \"Use grep -c (count only) or wc -l to inspect without exposing values.\" >&2; exit 2; fi'" }] }, { "matcher": "Grep", "hooks": [{ "type": "command", "command": "bash -c 'INPUT=$(cat); PATH_VAL=$(echo \"$INPUT\" | jq -r \".tool_input.path // empty\"); if echo \"$PATH_VAL\" | grep -qiE \"\\.(env|dev\\.vars|pem|key)$\"; then echo \"BLOCKED: Grep targeting secret file.\" >&2; exit 2; fi'" }] } ] } }What this does:
.env*,.dev.vars,.pem,.key,credentials,secretsgrep,cat,head,tail, etc. targeting those files (your exact reproduction case withgrep -n .dev.vars)Key design choice: exit code
2= hard block (tool call is rejected before execution). The model sees "BLOCKED" in the tool result and adapts. This is fundamentally different from CLAUDE.md which is guidance — hooks are enforcement.For the output-side scrubber you proposed (catching leaked secrets in tool results): that would require a PostToolUse hook that scans stdout for high-entropy strings. The challenge is false positives (base64 content, hashes, UUIDs). A PreToolUse approach (blocking before the read happens) is more reliable since it prevents exposure entirely rather than trying to redact after the fact.
If you want a more complete setup, you can also add a
write-secret-guardhook on the Edit/Write matchers to prevent Claude from writing secrets into source files — same pattern, different tool.RCushmaniii commentedon Apr 7, 2026
Incident Report: Claude Code Repeated Credential Exposure
Reporter: Robert F. Cushman III (CushLabs AI Services)
Date: 2026-04-07
Product: Claude Code (Anthropic CLI), Claude Opus 4.6 (1M context)
Project: ny-english-messenger-bot — Cloudflare Worker, client engagement
Related issues: #44868, #44909
Severity: High — recurring, undetected, default-on credential exposure in a paid developer product
Summary
In a single Claude Code session on 2026-04-07, the same user's API credentials were exposed into conversation history multiple times in roughly four hours, via two distinct mechanisms. Each exposure required a credential rotation. No production code was written and no deployment occurred during the session. The exposures continued after the user installed the documented mitigation (a
PreToolUseguard hook), because one of the failure modes is not gated by tool calls and the hook cannot see it.This is a representative incident in a longer pattern that has been visible to this user across many prior sessions.
Failure modes observed
1. Model-side: inspection commands print secret file contents
The model issued
grep -n curl .dev.varsto locate a malformed line in a.dev.varsfile the user had just edited.grep -nprints the matching line content along with line numbers. The matching line contained a real Cloudflare API token. The token was written to the conversation transcript and persisted in chat history.This is the failure mode tracked in #44868. The same family includes
cat,head,tail,awk,sed,xxd,strings,printf, and anygrep/rginvocation without-c/--count.Mitigations the user already had in place:
~/CLAUDE.mdrule prohibiting reading or echoing secret files~/.claude/hooks-guard-secrets.mjsPreToolUse hookWhy none of it caught the leak:
CLAUDE.mdrule is advisory text. The model knew the rule and ran the command anyway, then apologized after the fact.cat/head/tailbut did not listgrep,awk,sed,xxd, orstrings. It also matched only.env/.pem/.keypaths and did not know about.dev.vars.2. Harness-side:
FileChangednotifications inject file contents into contextAfter the user rotated the exposed Cloudflare token and pasted the new value into
.dev.vars, the act of saving the file in the editor triggered Claude Code'sFileChangednotification mechanism. The harness read the modified lines and emitted a system reminder containing the full contents of those lines, including:No tool call was made. No permission was requested. No hook fired. The user pressed Ctrl+S in their editor and the contents of the file were piped into the model's context as a system reminder, where they were persisted in conversation history.
This is the failure mode tracked in #44909.
Mitigations the user had in place:
PreToolUsehook from earlier in the session.dev.varslisted in.gitignoreWhy none of it helped:
PreToolUsehooks only fire on tool calls.FileChangednotifications are not tool calls..gitignoreis not consulted by the file watcher. There is norespectGitignoresetting that applies.FileChangedcontent injection.Sequence of events
.dev.varsgrep -n curl .dev.varsto locate the bad line.dev.varsFileChangedsystem reminder containing lines 15–23 of.dev.varsPreToolUseguard hook to closegrep/awk/sed/.dev.varsgapsFileChangedblocking hook, wires it into settings.json, tests itCredentials requiring rotation as a direct result of this session:
Productive development time during the session: zero. No code was written, no infrastructure was provisioned, no deployment occurred.
Root cause analysis
Both failure modes share one structural cause: Anthropic ships Claude Code with no harness-level enforcement against credential exposure. Every defensive mechanism documented to the user —
CLAUDE.mdrules,.gitignore, model self-monitoring — is advisory or operates at the wrong layer to catch the actual leaks.CLAUDE.mdrules.gitignorePreToolUsehooksFileChangedFileChangedhooksThere is no default-on, harness-enforced filename filter. There is no default-on output scrubber. There is no setting to disable
FileChangedcontent injection. There is no documentation warning users that editing a.envfile in their editor will leak its contents into chat history.Mitigations the user implemented during the session
These are the user's own work, performed mid-session with the agent's assistance. They are not Anthropic-supplied.
~/.claude/hooks-guard-secrets.mjs— full rewrite. Blockscat,head,tail,less,more,type,get-content,gc,select-string,sed,awk,xxd,od,strings,base64,cp,mv,scp,rsync,curl,wget,printf,echoagainst any path matching sensitive-file patterns. Allowsgrep/rgonly with-c/--count. Allowswc,stat,ls. Exempts.example/.sample/.templatesuffixes. Verified against 8 reproduction cases.~/.claude/hooks-block-filechanged-secrets.mjs— new file. Wired into theFileChangedevent in~/.claude/settings.json. BlocksFileChangednotifications for sensitive-file patterns. Verified against 5 reproduction cases.~/CLAUDE.md— paragraph added under "Secrets & Sensitive Data" documenting the existence of both hooks and instructing future Claude instances not to attempt to bypass them.Repo-level memory —
feedback_secrets_handling.mddocumenting the failure mode and the safe-command alternatives, so future sessions in this repository inherit the knowledge.These mitigations protect this user in this user's environment going forward. They do not protect any other Claude Code customer. Default behavior on a clean install of Claude Code remains exposed.
Recommended actions for Anthropic
Default-on filename filter for
FileChangedcontent injection. When a modified file matches a sensitive-pattern path (.env*,.dev.vars*,*.pem,*.key,id_rsa*,credentials*.json,service-account*.json, anything containingsecret/token/credential), the harness should emit metadata only, not content.Default-on output-side scrubber. Before any tool result or system reminder is appended to conversation history, run a regex pass over the content for known token shapes (
sk-,sk-ant-,cfut_,ghp_,gho_,xox[bp]-,AKIA[0-9A-Z]{16}, JWT shapes, PEM block markers) and replace matches with<REDACTED>placeholders.Respect
.gitignorefor the file watcher. Ship afileChanged.respectGitignoresetting defaulting totrue. Files ignored by git should not have their contents injected into the conversation transcript without explicit opt-in.User-facing kill-switch. Add
fileChanged.includeContent: falsefor users who want metadata-only notifications regardless of filename pattern.Documentation. Add a prominent security notice to the Claude Code docs explaining that the file watcher reads modified files and pipes their contents into the model's context, and how to disable or filter this.
Notification of affected users. Users whose past Claude Code sessions touched sensitive-pattern files should be notified so they can rotate proactively.
Audit and purge mechanism. Provide a way for affected users to identify which past transcripts contain leaked file contents, and to request purge from server-side storage and downstream telemetry.
Customer credit for incident-related cleanup time. Establish a policy for compensating customers whose paid sessions are consumed by mitigating Anthropic-side defects.
What this user is asking for
A response from Anthropic acknowledging:
A holding response within seven days. A substantive response within thirty.
This document was prepared by Claude Opus 4.6 in the same Claude Code session in which the incidents described above occurred, at the user's instruction. Technical claims are reproducible from the user's environment and from the public bug reports cited.
JoergAlthoff commentedon May 14, 2026
Same issue, two violations in one session:
Claude was asked to migrate secrets from .envrc to .env. The project's CLAUDE.md contains an explicit rule:
▎ "Niemals selbst einen Secret-Wert in den Chat schreiben oder vermuten." (Never write a secret value into the chat.)
Violation 1: Claude used the Read tool on .envrc and .env and output both files in full — all tokens visible in the chat
transcript (Paperless, Mistral, SonarQube API keys).
Violation 2: After being explicitly told about the violation, Claude immediately used the Edit tool and placed the token value
verbatim in the old_string parameter — again visible in the chat.
The rule was known, acknowledged, and broken twice in a row. There is no runtime enforcement — CLAUDE.md instructions are
treated as suggestions, not constraints.
localfirst-ai commentedon Jun 10, 2026
The FileChanged vector in the follow-up is worth pausing on. There is a
FileChanged hook event now, so the change itself is observable, and you can
react to it. What no hook can do, because no event in the hooks reference
covers it, is inspect or modify the assembled request body before it leaves.
So you can know the file changed, and still not know what actually entered
the context that went out. The one place that ground truth exists is the
outbound request itself.
That boundary is where I ended up enforcing after my own incident. I asked the
agent for a deploy command and explicitly told it not to connect to my Azure
server. It assumed my identity anyway, connected, and then read my
.env. Thepart that scared me was how close I came to not noticing the tool call at all.
I built a small open-source tool around that boundary (Occasio, local-first,
not affiliated with Anthropic). It is a local proxy in front of the API, so
enforcement happens on the outbound request body rather than per tool. A
denied path (.env, key files, credential dirs) is blocked identically for the
Read tool or any shell command that names the path. The request body is also
walked before it leaves: tool results that were baked into context without a
live tool call get stripped when their paired tool_use names a denied path,
and secret-shaped values in tool results get redacted even with no path to
tie them to, each with an audit row. Being precise about coverage: content
the harness injects as plain text rather than a tool result, which is what a
FileChanged notification looks like, is visible at the boundary today but not
yet auto-stripped. That exact shape is the strongest argument I know for your
ask: some of this can only be done properly native in the harness. Runtime
indirection (an interpreter constructing a path) is a documented residual
that falls through to the scrubber rather than the deterministic block.
Two additions my incident forced: identity borrows (ssh, cloud CLI, sudo) sit
behind an explicit human approval, so the agent can request an identity but
not silently assume one. And every block, strip, and redaction lands in a
tamper-evident local audit log, so the near-invisible tool call I almost
missed becomes a row you can't lose.
Fully agreed with the closing principle: the model can't police its own tool
calls, the harness has to enforce it. Until that exists native and default-on,
the outbound boundary is at least one place to do it consistently.
github.com/occasiolabs/occasio
chika5105 commentedon Jun 12, 2026
@RCushmaniii Given that models are probabilistic by nature, I do not think any prompt/instruction file / pretool harness will eliminate this problem. We need to move to structural confinement, where agents never have access to any keys in the first place. I'm building raxis.io, a governed runtime for AI agents. RAXIS runs agents without raw credentials & direct network access; every privileged action goes through a host-side policy kernel, and the system records a tamper-evident trail of what was allowed or denied. I’m interviewing people who’ve seen real agent failure modes up close and looking for early design partners to shape the product around the guarantees teams actually need. Would you be open to a 20-minute research conversation?
github-actions commentedon Jul 24, 2026
Closing for now — inactive for too long. Please open a new issue if this is still relevant.
nnWhisperer commentedon Jul 25, 2026
I think that the issue must still be active, this is a feature that must be supported by claude code
softcane commentedon Jul 30, 2026
Until these types of get fixed at the model or harness's level, community have to grouped together start building proper safe guards.
I started this on the same note https://github.com/softcane/hamza
github-actions commentedon Sep 23, 2026
This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.