Skip to content

Claude Code exposes secrets from .env / .dev.vars files via grep -n and Read tool, despite CLAUDE.md prohibitions #44868

Closed as not planned

Description

@RCushmaniii

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

  1. User has a global CLAUDE.md containing rules like:

    NEVER output secret values into chat. NEVER read .env files unless strictly necessary, and never reproduce any values in your response.

  2. User asks Claude Code to help populate a .dev.vars file with API tokens.
  3. User accidentally pastes a malformed value (e.g. a curl example command) instead of a bare token.
  4. Claude Code attempts to diagnose by running:
    grep -n curl .dev.vars
    
  5. grep -n prints the full matching line, including the real API token, into the tool result — which is rendered into the conversation transcript and persisted in history.
  6. 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.md instructions 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 -n for 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:

  1. 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-only awk patterns, blind Edit with literal old_string). Block everything else by default.
  2. 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.
  3. 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.
  4. 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.vars file.
  • 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.md with explicit secret-handling rules
  • User confirmed reproducibility immediately after the incident

Activity

  1. github-actions commented on Apr 7, 2026

    @github-actions

    Found 3 possible duplicate issues:

    1. Security: Claude displays full credential file contents when checking for stored tokens #34819
    2. SECURITY: Claude Code reveals secret keys and credentials in terminal output despite explicit prohibitions #32523
    3. [BUG] Claude Code reads .env files and hardcodes secrets into inline scripts #24185

    This issue will be automatically closed as a duplicate in 3 days.

    • If your issue is a duplicate, please close it and 👍 the existing issue instead
    • To prevent auto-closure, add a comment or 👎 this comment

    🤖 Generated with Claude Code

  2. yurukusa commented on Apr 7, 2026

    @yurukusa

    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:

    • Read hook: Blocks reading any file matching .env*, .dev.vars, .pem, .key, credentials, secrets
    • Bash hook: Blocks grep, cat, head, tail, etc. targeting those files (your exact reproduction case with grep -n .dev.vars)
    • Grep hook: Blocks the dedicated Grep tool from targeting secret files
      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-guard hook on the Edit/Write matchers to prevent Claude from writing secrets into source files — same pattern, different tool.
  3. RCushmaniii commented on Apr 7, 2026

    @RCushmaniii
    Author

    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 PreToolUse guard 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.vars to locate a malformed line in a .dev.vars file the user had just edited. grep -n prints 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 any grep/rg invocation without -c / --count.

    Mitigations the user already had in place:

    • Global ~/CLAUDE.md rule prohibiting reading or echoing secret files
    • Pre-existing ~/.claude/hooks-guard-secrets.mjs PreToolUse hook

    Why none of it caught the leak:

    • The CLAUDE.md rule is advisory text. The model knew the rule and ran the command anyway, then apologized after the fact.
    • The pre-existing PreToolUse hook had gaps: it blocked cat/head/tail but did not list grep, awk, sed, xxd, or strings. It also matched only .env/.pem/.key paths and did not know about .dev.vars.

    2. Harness-side: FileChanged notifications inject file contents into context

    After 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's FileChanged notification mechanism. The harness read the modified lines and emitted a system reminder containing the full contents of those lines, including:

    • Qdrant Cloud URL
    • Qdrant API key (full value)
    • Cloudflare account ID
    • Newly-rotated Cloudflare API token (full value)

    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:

    • Same as above, plus the rewritten PreToolUse hook from earlier in the session
    • .dev.vars listed in .gitignore

    Why none of it helped:

    • PreToolUse hooks only fire on tool calls. FileChanged notifications are not tool calls.
    • .gitignore is not consulted by the file watcher. There is no respectGitignore setting that applies.
    • There is no built-in filename filter, content scrubber, or user-facing kill-switch for FileChanged content injection.

    Sequence of events

    # Event Result
    1 User pastes a malformed Cloudflare token (curl example) into .dev.vars File is broken
    2 Agent runs grep -n curl .dev.vars to locate the bad line Cloudflare token written to transcript (Exposure #1)
    3 User rotates Cloudflare token, pastes new value, saves file New token in .dev.vars
    4 Harness emits FileChanged system reminder containing lines 15–23 of .dev.vars New Cloudflare token + Qdrant API key written to transcript (Exposure #2)
    5 Agent rewrites PreToolUse guard hook to close grep/awk/sed/.dev.vars gaps Hook tested and live
    6 Agent writes FileChanged blocking hook, wires it into settings.json, tests it Hook tested and live
    7 User instructs agent to file public bug reports and incident document #44868, #44909, this report

    Credentials requiring rotation as a direct result of this session:

    • Cloudflare API token: rotated twice
    • Qdrant API key: rotated once

    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.md rules, .gitignore, model self-monitoring — is advisory or operates at the wrong layer to catch the actual leaks.

    Defense Layer Coverage
    CLAUDE.md rules Model prompt Advisory only; model can and does ignore in-the-moment
    Model self-monitoring Model output Fires after the secret has already been produced
    .gitignore Git tooling Not consulted by Claude Code's file watcher
    PreToolUse hooks Harness, before tool calls Only catches tool-call leaks; does not see FileChanged
    FileChanged hooks Harness, before notifications Exists but undocumented as a security mechanism; users do not know to use it

    There is no default-on, harness-enforced filename filter. There is no default-on output scrubber. There is no setting to disable FileChanged content injection. There is no documentation warning users that editing a .env file 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.

    1. ~/.claude/hooks-guard-secrets.mjs — full rewrite. Blocks cat, head, tail, less, more, type, get-content, gc, select-string, sed, awk, xxd, od, strings, base64, cp, mv, scp, rsync, curl, wget, printf, echo against any path matching sensitive-file patterns. Allows grep/rg only with -c/--count. Allows wc, stat, ls. Exempts .example/.sample/.template suffixes. Verified against 8 reproduction cases.

    2. ~/.claude/hooks-block-filechanged-secrets.mjs — new file. Wired into the FileChanged event in ~/.claude/settings.json. Blocks FileChanged notifications for sensitive-file patterns. Verified against 5 reproduction cases.

    3. ~/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.

    4. Repo-level memory — feedback_secrets_handling.md documenting 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

    1. Default-on filename filter for FileChanged content injection. When a modified file matches a sensitive-pattern path (.env*, .dev.vars*, *.pem, *.key, id_rsa*, credentials*.json, service-account*.json, anything containing secret/token/credential), the harness should emit metadata only, not content.

    2. 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.

    3. Respect .gitignore for the file watcher. Ship a fileChanged.respectGitignore setting defaulting to true. Files ignored by git should not have their contents injected into the conversation transcript without explicit opt-in.

    4. User-facing kill-switch. Add fileChanged.includeContent: false for users who want metadata-only notifications regardless of filename pattern.

    5. 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.

    6. Notification of affected users. Users whose past Claude Code sessions touched sensitive-pattern files should be notified so they can rotate proactively.

    7. 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.

    8. 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.

  4. JoergAlthoff commented on May 14, 2026

    @JoergAlthoff

    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.

  5. localfirst-ai commented on Jun 10, 2026

    @localfirst-ai

    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. The
    part 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

  6. chika5105 commented on Jun 12, 2026

    @chika5105

    @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?

  7. github-actions commented on Jul 24, 2026

    @github-actions

    Closing for now — inactive for too long. Please open a new issue if this is still relevant.

  8. nnWhisperer commented on Jul 25, 2026

    @nnWhisperer

    I think that the issue must still be active, this is a feature that must be supported by claude code

  9. softcane commented on Jul 30, 2026

    @softcane

    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

  10. github-actions commented on Sep 23, 2026

    @github-actions

    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.

  11. locked as resolved and limited conversation to collaborators on Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions