The Permission Prompt That Leaks
Every Claude Code user knows the button. Claude proposes a shell command, a prompt asks Do you want to run this? The tenth time in a session, you stop reading carefully and click the fast lane: “Yes, and don’t ask again.”
That one click is the heart of the Claude Code settings.local.json secrets leak — a story where the permission log meant to keep humans in control quietly became a plaintext credential store, and 1,122 such files ended up on GitHub holding real secrets.
Here’s what the click actually does. Claude Code saves the approval as an allow rule in .claude/settings.local.json, inside your project folder. Per Anthropic’s settings documentation, the file is created “the first time you give a standing approval on a permission prompt, such as ‘Yes, and don’t ask again’ for a Bash command.”
And the rule it saves is not a pattern. Per Apiiro’s research, “The rule is the command string itself” — the full, verbatim command, including “whatever you typed into it: look for passwords, tokens, keys and connection strings.”
If the command carried credentials — a database password in a connection string, an API key in a curl header — the secret is now a literal string in a JSON file inside your repository. The safety feature designed to keep humans in control became the exfiltration path.

How the Claude Code settings.local.json Secrets Leak Happened
The mechanics matter, because the failure isn’t model hallucination or prompt injection. It’s deterministic file plumbing.
Developer machines are full of secrets: a connection string in a config file, a cloud key in an installed CLI, a token in .env. Claude Code can’t run a database check without them, so the command it proposes carries the secret. You see the whole command in the approval dialog — but by the tenth prompt, a password is just one word in a long line. Apiiro put it precisely: approving “runs the command and adds a permission rule to .claude/settings.local.json in the project folder, so it won’t ask again.”
An affected file, with illustrative (fabricated) values, looks like this:
1 | { |
This is Claude Code’s own behavior, not something the model improvised — Apiiro reproduced it “with Opus 5.5 and Fable 5.1.” And when the team compared how 16 coding agents store approvals from their documentation, Claude Code was “the only one confirmed to write the full approved command to a project file by default.”
The file itself was never a scandal — it’s meant to stay local, and a machine-local allow list is reasonable. The scandal is what happens one commit later.
Why .gitignore Couldn’t Save You
Anthropic does try to keep the file out of your commits. The settings docs spell out the mechanism:
The first time Claude Code writes the file in a git repository that doesn’t already ignore it, it adds
**/.claude/settings.local.jsonto your global git excludes file, so the file stays out of your commits in every repository.
Note the word global. In code:
1 | # Where the protection lives — outside the repo: |
That’s the flaw. Git has three layers of ignore: the repo’s .gitignore, which travels with the project; and .git/info/exclude and the global excludes file, which live on your machine. Claude Code chose the user-global layer. The protection covers “the common case,” as Apiiro puts it — the original repo on the machine where the file was created. But:
- The folder becomes a git repo later. The file gets written before
git init, and nothing carries the exclude forward. - You clone on another machine. A new machine gets a fresh global excludes file. The rule never went into the repo’s own
.gitignore, so, in Apiiro’s words, “nothing travels with the project to protect it.” - The file is already tracked. Git ignores tracked files’ ignore rules entirely. Claude Code notices — its debug log records that the file “is tracked in the index; gitignore rules do not apply to tracked files” — but per Apiiro, “The user is not told, and the next approval is appended to the tracked file.”
So the ignore protection is real but not portable: it lives on the developer, not the repository. Ars Technica’s framing: the global ignore “isn’t reflected at the repo level, meaning the sensitive file can be uploaded accidentally if a repo is cloned to another machine or a folder becomes a repo later.”
There’s an ugly twist: sometimes Claude Code commits the file itself. In 539 of the 1,105 repos with a real secret that Apiiro traced, “a commit co-authored by Claude changed the file.” Claude Code’s commit instructions tell the model not to commit .env or credentials.json — its own settings file is not on that list.
What Apiiro Found on GitHub
Apiiro researchers Shmulik Cohen and Matan Giladi ran a read-only GitHub code and commit search across default branches, November 1–3, 2026 — slicing queries by file size to work under GitHub code search’s 1,000-result cap, and downloading every copy they listed. The census of the Claude Code settings.local.json secrets leak:
- 104,185 public copies of
.claude/settings.local.json, across 103,617 repositories. - 1,122 files — 1.1% — hold a real secret. That’s the small denominator: files with at least one credential.
- Together those files hold 1,379 distinct secrets: 636 provider-format keys and 743 passwords or tokens sitting next to a public host.
Those three numbers get mangled in every repost, so keep them straight: 104,185 copies exist publicly; 1,122 of those contain a secret; 1,379 is the count of distinct secrets they contain.
And git history makes it worse. Counting deleted versions of the file, teams’ shared settings.json, and agent chat transcripts saved in repos, the total rises to 2,539 distinct secrets across 1,994 repositories. Earlier versions hold 734 real secrets in 509 repos that appear in no current copy — usually because someone deleted the file and left history alone. Forks compound the surface: 211 forks of 74 affected repos carry the same secrets.
What leaked, per Apiiro’s classification: 379 database passwords and 122 Supabase service-role keys (plus 103 Supabase account tokens, which control every project in an account); 43 SSH credentials for public servers, some root passwords inside sshpass commands; 42 GitHub, GitLab and npm tokens; 23 AWS keys; 6 private keys. “More than a third open a database directly.”
Who was exposed: 63 organizations, “from student groups to startups, software agencies and established vendors” — including a database vendor’s own admin-level cloud key and a production Azure Postgres password. Apiiro tested no credentials, but “One confirmed its key had been live.”

The exposure is also long: in the median affected repo the file was first committed 217 days before the scan, and the secret-hit rate scales with how long you’ve been clicking — one in ten files over 8 KB holds a secret, versus fewer than one in 500 under 500 bytes.
Meanwhile the scanners mostly whiffed. Apiiro ran gitleaks and TruffleHog over all 104,185 files: of the 1,122 secret-bearing files, gitleaks flagged 564, TruffleHog 643 — and 215 were flagged by neither. The gap is passwords inside commands: “a password typed after PGPASSWORD=, sshpass -p or mysql -p”, or a bearer header in a curl call, is an ordinary word in an ordinary command line." It’s a familiar shape for anyone who followed the OpenCode API key breach earlier this year: agent toolchains leaking credentials nobody thought to scan for. Remember that for anything else an agent records — transcripts, memory files, handoff notes.
The response after publication was fast, and ugly in its own way. On the November 9 SANS Stormcast, Johannes Ullrich reported that SANS web honeypots had, “for the first time,” seen requests for the .claude directory: “So attackers are definitely looking for it.” GitLab published a mitigation guide on November 11 for finding and blocking the file in GitLab projects. The bug is now a scanning target and a policy checklist, not just a research finding.
The Fix: Claude Code 2.1.321 → 2.1.323
Anthropic responded first with words. Boris Cherny, head of Claude Code, joined the HN thread (626 points, ~420 comments as of Nov 13) on November 7 and kept commenting through the weekend: “Historically Claude Code has not done secret scrubbing (there are many good tools for that), but we are looking into what we can do to prevent this.” So Anthropic publicly acknowledged the issue — though it also argued the file is gitignored automatically “when Claude creates the file, so in this case the files were manually created and checked in.” Apiiro’s commit data suggests the real-world failure modes were broader than that.
Then came the code. Claude Code shipped 2.1.321 and 2.1.322 on November 9–10 (npm timestamps) with no entries touching the secrets leak. The hardening landed in 2.1.323, published November 12, and its changelog entries map one-to-one onto Apiiro’s failure modes:
Changed “don’t ask again” so an allow rule that looks like it holds a secret, or would be saved to a
settings.local.jsonthat git tracks, lasts for the session only and isn’t saved.
Fixed
.claude/settings.local.jsonnot being ignored globally when the folder became a git repository later, or on a new machine.
That second entry is the repo-lifecycle gap, closed in the tool itself. The first is a write-path fix: instead of persisting an approval it deems secret-bearing or destined for a tracked file, Claude Code keeps it in memory for the session only. The release also hardened adjacent credential surfaces:
Changed Claude Code’s own ripgrep and most internal git commands to run without most of its sign-in tokens and API keys, so a credential helper or hook under such a git command no longer finds them.
Changed start-up to stop when a
--settingsfile isn’t valid JSON and has asandboxkey, even one that turns the sandbox off.
Plus, among the fixes: claude -p and the Agent SDK no longer run Bash unsandboxed when sandbox-enabling --settings fail validation, and Bash prompts no longer offer “don’t ask again” for commands writing to sensitive paths like .git or .claude where the saved rule didn’t take effect.
One honesty note: nothing in the changelog names Apiiro’s research as the motivation [UNVERIFIED: any Anthropic statement explicitly tying the 2.1.323 release to the Apiiro findings or the HN discussion]. But the mechanics match the disclosure almost line for line — the ignore gap, the tracked-file case, secret-looking allow rules, Claude Code’s own tokens in git subprocesses. Whether it’s the promised “prototype” is unstated; whether it’s a fix is plain.
What You Should Do Today
If you use Claude Code — or any agent that persists approvals — run this checklist in order. The Claude Code settings.local.json secrets leak is, ultimately, a checklist-shaped problem, and Apiiro’s advice has the right ordering: rotate first, clean up second.
- Check what’s public now. Search your GitHub organization for
.claude/settings.local.jsonand.claude/settings.json— “Each hit is a file anyone can read right now.” - Check history, not just the working tree.
git log --all --full-history -- .claude/settings.local.jsonon every repo — deletion doesn’t help. Apiiro found 211 forks of 74 affected repos carrying the same secrets. - Rotate every credential the file has ever held. GitHub’s guidance puts it bluntly: “as a first step you need to revoke and/or rotate that secret” — removal alone doesn’t clear forks, clones, or cached views, where secrets survive “in any clones or forks of your repository.”
- Untrack the file where it’s tracked:
git rm --cached .claude/settings.local.json, then add the path to the repo’s.gitignore— the layer that travels. - Upgrade Claude Code to ≥ 2.1.323 on every machine, including headless
claude -pand SDK installations. - Audit local files even if nothing was committed. Delete every rule containing a secret, and check the file on each clone — remember, excludes are per-machine.
- Tune your scanners. Standard key-format scanners missed 215 of Apiiro’s 1,122 files. Treat agent folders (
.claude/,.cursor/,.gemini/, transcripts, memory files) as secret-bearing like.env, and make sure pre-commit/CI scanning catches passwords inside command lines, not only provider key prefixes. - Change how you approve. Approve command patterns like
Bash(npm run:*)rather than literal commands, and reference secrets as$VAR, so the rule records the variable name, not the value.
Even with the fix shipped, this maps onto our earlier coverage of vibe coding security risks: AI-accelerated development quietly widens the attack surface of AI apps, and rotating exposed credentials after a data leak is the step teams skip. This incident is the same class of problem wearing a new file name.
Design Lessons for Agent Permission Systems
One honest caveat on the timeline: GitHub issues dating back to August 2025 had already documented this exact mechanism, and none had drawn a reply from Anthropic until the November 2026 disclosure.
Step back, and this incident is about a pattern that will keep recurring: agent state files become credential stores. Claude Code never intended to store secrets — it stored commands, and commands are where developers put secrets. Any interface that persists what the user’s input contained inherits everything the input contains. There is no such thing as “just an allow rule.”
Three design principles fall out:
Persistence policy on approval state. “Don’t ask again” used to mean remember forever, on disk, in the project directory. In 2.1.323, secret-looking or tracked-file-bound approvals last “for the session only.” Product teams should steal that instinct: permission memory should start with the shortest scope that solves prompt fatigue, and grow to disk scope only if the content is provably secret-free.
Secret-scanning on the write path, not just the repo. GitHub’s push protection, gitleaks, TruffleHog — all inspect repositories. Claude Code’s approval engine is a write path that never went through them. The 215 files that slipped past both scanners show scanners must broaden (passwords-in-commands, not just key prefixes) — but the deeper fix is at the point of persistence: when a rule “looks like it holds a secret,” don’t write it.
Repo-lifecycle awareness for “local-only” claims. The global-excludes protection worked perfectly on the machine where the file was born. It failed on every other machine and on every repo created after the file. Any “local-only” guarantee that relies on machine-local state must answer: what happens on the next clone, on a new laptop, when the folder becomes a repo later? “Local” is a property of the machine; “uncommitted” is a property of the repository. Only .gitignore travels.
And the general pattern echoes an incident from weeks earlier, part of the Railway black October wave of agentic-AI infrastructure breaches. In October, an attacker breached Railway’s infrastructure — “the attacker accessed credentials to access our CI environment where the attacker was able to decrypt artifacts,” leading to “an unauthorized query of encrypted environment variables, of which a subset were decrypted,” all while hunting AI provider API keys. The env-variable store, where customers’ secrets concentrate, was the objective; the blast radius was “at most 1.85% of all workspaces” (about 100,000), and “all of the keys we stored, for all users on Railway” went to some vendors for precautionary revocation. The agent version is already here: an allowlist is a developer’s own miniature env store. Centralize credentials anywhere — a platform’s variable store, an agent’s approval log — and that store becomes what attackers enumerate. It’s the same threat family as the trapdoor supply-chain attack on an AI coding assistant: the weakest link is wherever agent-controlled credentials concentrate.
The Takeaway
The uncomfortable part of this story isn’t that an agent wrote a file. It’s that the leak lived in the human-in-the-loop machinery itself. The permission prompt existed to keep a person in the loop; “don’t ask again” existed to respect the person’s time; and the record of that judgment — the exact command they waved through on a tired afternoon — put their credentials on GitHub, in 1,122 public files, for a median of 217 days.
The Claude Code settings.local.json secrets leak won’t be the last time an agent’s own state file turns hostile. It joins a lineage — from the trapdoor supply-chain attack to the Railway breach — where the tooling built for trust became the path to theft. Anthropic’s response has been, on balance, a decent template: engage publicly, argue your position, ship mechanics-level fixes within a week. But fixes are reactive by definition. The durable question for every agent product: when your agent records what it ran, assume it’s recording your secrets — and design the write path as if that’s already true. The audit trail and the credential breach are the same artifact, unless someone — human or model — refuses to persist the dangerous parts.
References and further reading
- Apiiro: Coding Agents Copy Your Secrets to a Hidden File. Thousands Are on GitHub — the original research by Shmulik Cohen and Matan Giladi
- Anthropic Claude Code settings documentation — how
settings.local.jsonand the global git exclude work - Claude Code changelog — release notes including 2.1.321 through 2.1.323
- Claude Code v2.1.323 GitHub release — the hardening release
- @anthropic-ai/claude-code on npm — package page with publish timestamps
- SANS Stormcast, November 9, 2026 — Johannes Ullrich on honeypot requests for the
.claudedirectory - GitLab: How to prevent leaks from Claude Code’s ‘Don’t ask again’ file — mitigation guide for GitLab projects
- GitHub secret scanning documentation — revoking, rotating, and scanning exposed secrets
- Hacker News discussion — thread including responses from Boris Cherny of the Claude Code team
- Railway Incident Report: October 21st Security Attack — the detailed post-mortem of the earlier infrastructure breach
Please let us know if you enjoyed this blog post. Share it with others to spread the knowledge! If you believe any images in this post infringe your copyright, please contact us promptly so we can remove them.