claude-code - 💡(How to fix) Fix Permission allowlist caches Bash command strings with inline secrets — settings.json persists PINs / tokens / passwords across sessions

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

When a user approves a Bash command containing an inline secret (an env-var assignment like SECRETS_PIN=123456 openssl ..., a --token <hex> argument, or any other inline credential), Claude Code writes the literal command string into permissions.allow in the project's .claude/settings.json. Future sessions read settings.json at startup. The allowlist feature, intended to reduce permission prompts, instead persists the very secret it was protecting.

This file is often checked into version control. If .claude/settings.json is committed (intentionally or via a wildcard like git add .), the leak persists in git history even after redaction — only git filter-branch or BFG fully expunges it.

Root Cause

When a user approves a Bash command containing an inline secret (an env-var assignment like SECRETS_PIN=123456 openssl ..., a --token <hex> argument, or any other inline credential), Claude Code writes the literal command string into permissions.allow in the project's .claude/settings.json. Future sessions read settings.json at startup. The allowlist feature, intended to reduce permission prompts, instead persists the very secret it was protecting.

This file is often checked into version control. If .claude/settings.json is committed (intentionally or via a wildcard like git add .), the leak persists in git history even after redaction — only git filter-branch or BFG fully expunges it.

Code Example

SECRETS_PIN=123456 openssl enc -aes-256-cbc ...

---

"permissions": {
     "allow": [
       "Bash(SECRETS_PIN=123456 openssl enc -aes-256-cbc -pbkdf2 ...)"
     ]
   }

---

SECRET_KEY_PATTERNS = [r'(?i)(PIN|PASSWORD|TOKEN|KEY|SECRET|PASSPHRASE|API_KEY)']
# Match KEY=VALUE pairs where KEY matches; replace VALUE with <REDACTED>
normalised = re.sub(
    r'\b((?:' + '|'.join(SECRET_KEY_PATTERNS) + r')[A-Z_]*?)=\S+',
    r'\1=<REDACTED>',
    command
)

---

Allow: Bash(SECRETS_PIN=$PIN openssl enc ...) :NOCACHE
RAW_BUFFERClick to expand / collapse

Description

When a user approves a Bash command containing an inline secret (an env-var assignment like SECRETS_PIN=123456 openssl ..., a --token <hex> argument, or any other inline credential), Claude Code writes the literal command string into permissions.allow in the project's .claude/settings.json. Future sessions read settings.json at startup. The allowlist feature, intended to reduce permission prompts, instead persists the very secret it was protecting.

This file is often checked into version control. If .claude/settings.json is committed (intentionally or via a wildcard like git add .), the leak persists in git history even after redaction — only git filter-branch or BFG fully expunges it.

Reproduction

  1. User runs a Bash command with an inline secret:
    SECRETS_PIN=123456 openssl enc -aes-256-cbc ...
  2. Claude Code prompts for permission. User approves.
  3. Inspect the project's .claude/settings.json:
    "permissions": {
      "allow": [
        "Bash(SECRETS_PIN=123456 openssl enc -aes-256-cbc -pbkdf2 ...)"
      ]
    }
  4. The literal PIN is now persisted on disk, readable by anything with file-system access, and auto-loaded into context at the start of every future session in this project.

Actual incident

A user pasted their master vault PIN inline into chat to test an autonomous vault-storage workflow. Three openssl commands using SECRETS_PIN=<pin> were approved during the session. All three were written verbatim into <project>/.claude/settings.json. The PIN landed in four files total:

  1. .claude/settings.json allowlist (this issue)
  2. The Claude Code project transcript JSONL at ~/.claude/projects/<project-slug>/<session-id>.jsonl
  3. A persona-state JSONL at ~/.claude/persona-state/<session-id>.jsonl
  4. Daily-log capture written by a project hook

Files 1-3 are owned by Claude Code itself. The user redacted all four manually after-the-fact via sed, but local-disk redaction does NOT undo any cloud-backup sync (Time Machine / iCloud Drive / Dropbox / GitHub if checked in).

Why this is high-impact

  • It defeats the user's mental model of the allowlist. Users approve a command thinking they're whitelisting a pattern; instead they're whitelisting a concrete string with secrets baked in.
  • Reduce-prompts feature becomes a leak vector. The very mechanism designed to make Claude Code more usable is the mechanism that persists the secret.
  • Affects every project, not just the one where the user typed the secret. Project-local settings.json files are often committed; the secret crosses into git history.
  • Silent failure mode. No warning at approval time that the command contains a likely secret; no mention that the literal string will be persisted.

Suggested fix options (designer's choice)

In order of cheapest-to-most-thorough:

(a) Allowlist normaliser — mask KEY=VALUE pairs whose KEY matches secret-name heuristics

Before writing to permissions.allow, run the command string through a normaliser:

SECRET_KEY_PATTERNS = [r'(?i)(PIN|PASSWORD|TOKEN|KEY|SECRET|PASSPHRASE|API_KEY)']
# Match KEY=VALUE pairs where KEY matches; replace VALUE with <REDACTED>
normalised = re.sub(
    r'\b((?:' + '|'.join(SECRET_KEY_PATTERNS) + r')[A-Z_]*?)=\S+',
    r'\1=<REDACTED>',
    command
)

Cached entries remain readable (so future approval prompts can still pattern-match) but the secret is gone.

(b) Opt-in :NOCACHE flag

Let the user mark a command as non-cacheable at approval time:

Allow: Bash(SECRETS_PIN=$PIN openssl enc ...) :NOCACHE

Approval is one-shot; nothing is written to settings.json. Default behaviour can stay the same; this is just an escape hatch for commands the user knows contain secrets.

(c) Hash-based allowlist with sensitivity bit

Cache the SHA-256 of the command for matching, not the literal string. For sensitive commands (detected by heuristic), require fresh approval each invocation regardless of hash match.

(d) Pre-write secret detector with user prompt

Before writing any allowlist entry, scan the command for secret-like patterns (high-entropy strings, KEY=VALUE assignments with secret-y key names, hex runs ≥ 32 chars). If detected, prompt the user:

"This command appears to contain a secret. Allow it for this session only? (Y/n)"

Combine with (a) as a belt-and-braces approach: even if the user clicks Yes, normalise before writing.

Minimum bar

At minimum, (a) — even without (b)(c)(d) — prevents the verbatim leak. It's a single regex pass on the write path; behaviour-preserving for non-secret commands.

Impact

  • Severity: High for users who type credentials inline (vault unlock workflows, ad-hoc openssl, custom encryption, API tokens passed as Bash args).
  • Severity: Medium for users who only use environment variables loaded from .env files (the secrets never enter the command string).
  • Privacy regression for any user whose project settings.json is committed to a shared repo.

Related

The same incident also implicates the transcript writer (file 2) and persona-state writer (file 3) — same underlying class of problem (no pre-write secret filter at any write path Claude Code controls). If a single PR addresses the broader pattern (a write-path secret filter applied across settings.json, the project transcript, and persona-state JSONL writes), all three leak vectors close together.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING