I approve shell commands faster than I read them. By the tenth prompt in a session I am pattern matching on the first few words, and “Yes, and don’t ask again” is what makes the prompts stop. That habit is the precondition for what Apiiro published on November 5: 1,122 public copies of a Claude Code settings file contain a real secret, put there by someone clicking the option the tool offered.
No CVE, no malicious package, no attacker in the loop. Just a permission system writing down what it was told to remember, in a format that includes the password you typed into the command.
🔍 What the approval actually stores
Anthropic’s permissions docs describe the flow without ambiguity: Claude Code asks before it runs most shell commands, and if you pick “Yes, and don’t ask again,” it “saves the rule to .claude/settings.local.json at the root of the git repository,” where it applies to future sessions anywhere in that repository, including worktrees.
What matters is what the rule contains. A Bash rule with no wildcard “matches one exact command.” So if Claude proposed PGPASSWORD=hunter2 psql -h db.example.com -U app, the saved allow rule is that string, password included. Approve a pattern like Bash(npm run *) and the rule is generic. Approve a one-off command with a connection string and the repo records the secret.
Apiiro says this is Claude Code’s behavior rather than the model’s, and that it reproduced it on Anthropic’s current models, Opus 5.5 and Fable 5.1. You cannot prompt this away. The agent is faithfully recording the decision you made.
🧱 The protection sits on the wrong machine
Claude Code does try to keep the file out of git, and the docs are precise about how. The first time it writes the file in a repository that does not already ignore it, it adds **/.claude/settings.local.json to your global git excludes: core.excludesFile if you set one, otherwise ~/.config/git/ignore.
That exclusion is a per-user file on one machine. It is not in the repository. It does not travel with a clone, does not exist on a teammate’s laptop, does not apply to a folder that became a git repository after the file was written, and does nothing to a file that is already tracked. Git ignores do not untrack. Anthropic’s docs say outright that if you created the file by hand, you add it to .gitignore yourself.
The tracked case matters because the tool can see it. The permissions page says that when the file is tracked in git, Claude Code treats it as repository-supplied and holds its rules until you trust the folder. Apiiro reports that the debug log records “is tracked in the index; gitignore rules do not apply to tracked files,” that the user is not told, and that the next approval is appended anyway.
Then there is the detail I keep circling. In 539 of the 1,105 repositories 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 the scan found
Apiiro collected public GitHub code and commit search between November 1 and 3, working around the 1,000-result search cap, and counted 104,185 copies of .claude/settings.local.json across 103,617 repositories. Of those, 1,122 files, about 1.1%, hold a real secret: 1,379 distinct credentials, split into 636 provider keys and 743 passwords or tokens next to a public host.
More than a third open a database directly: 379 database passwords and 122 Supabase service-role keys. Another 103 are Supabase account tokens that control every project in an account. The rest includes 43 SSH logins for public servers, some a root password inside an sshpass command, 42 GitHub, GitLab and npm tokens, 23 AWS keys, and 6 private keys.
The cases are concrete: a database vendor’s own cloud API key with manage access and no expiry claim, a production Azure Postgres password, a Square production token, and the owner password of a medical-records database. 63 organizations in total, from student groups to vendors exposed through demo repos.
The age is what hurts. Every file with a secret sat on its default branch as of November 2, and the median repository first committed it 217 days earlier, more than a year before in 118 of them. Forks copy the problem: 211 forks of 74 repositories carry the same secrets, and earlier versions hold 734 more across 509 repositories that appear in no current copy.
Counting the shared .claude/settings.json that teams commit on purpose (298 of 76,011 copies have a secret), transcripts, memory files, handoff notes and Copilot settings, the total reaches 2,539 secrets in 1,994 repositories.
The limits are stated: no credential was used or tested, no host was contacted, and “real secret” means it matches a provider format or points at a public host, not that it works. One company did confirm its key had been live.
🕳️ Your scanner probably does not see this
Apiiro ran gitleaks and TruffleHog over all 104,185 files. Of the 1,122 with a real secret, gitleaks flagged 564 and TruffleHog flagged 643. Neither flagged 215 of them, roughly one in five.
The reason is structural. Both tools recognize key formats: a provider prefix, a long random string, a checksum. A password after PGPASSWORD=, sshpass -p, mysql -p, or in a bearer header inside a curl call is an ordinary word in an ordinary command line, and both tools mostly let it through. That shape appears wherever an agent records what it ran: transcripts, memory files, handoff notes.
Attackers already know this. The Shai-Hulud npm worm downloaded TruffleHog and ran it over infected developer machines in September 2025, and that is the scanner that misses 215 of these files.
A file that never reaches GitHub still holds plaintext credentials in a project folder, and it can leave by npm package, Docker image, zip archive or synced directory.
⚖️ The response, and where it lands short
Apiiro shared the research with Anthropic before publication. Boris Cherny, Anthropic’s head of Claude Code, responded on Hacker News on November 7. Apiiro quotes him: “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.”
The approaches, in his words: “making sure auto mode catches this, building in basic secret scanning, a few other ideas,” to be prototyped before anything ships. When a reader pointed out that auto mode can hit the same problem when it asks about a sensitive command, he answered, “Yeah good catch.”
His position is that Claude Code gitignores the file when it creates it, so the exposed copies were created by hand and checked in, and that people should prefer auto mode and host-side scanning.
Part of that is defensible. Hand-created files are a real path. But “it’s gitignored” is true only on the machine that wrote the exclude, and 539 of 1,105 traced repositories had Claude’s own co-authored commit touching the file. A user hand-editing JSON and committing it is plausible once. It is not a satisfying explanation for half the set.
The community dispute is fair: you have to be doing several things wrong at once. But the failure needs no attacker, no prompt injection and no compromised dependency, only clicking the option the tool presents after a long day of clicking. That is the standard a default should be held to.
The report history is the worst part. Users filed GitHub issues about this in August 2025 (#5544, #11982, #13106, #91270). A bot closed all four, and #5544 went out as “not planned” on January 3.
🛠️ What I would actually do
Order matters: rotate rather than delete, because removing a file does not clear history, forks or clones.
- Rotate every password, token and key the file has ever held, then untrack it with
git rm --cached .claude/settings.local.jsonand add it to the repository’s own.gitignoreso the protection travels with the project. - Search the whole organization for
.claude/settings.local.jsonand.claude/settings.json, and check history in repos where the file is gone today. - Open the file on your machine even if it was never committed, and delete every allow rule that contains a secret.
- Stop typing secrets into commands. Reference
$DB_URLso the saved rule records a variable name. - Approve patterns such as
Bash(npm run *)instead of literal one-off commands.
For scanning, add .claude/, .cursor/ and .specstory/ to pre-commit and CI checks including history, and make the rules catch passwords inside commands, not only provider key formats. Triage credentials paired with a public host first.
🧠 Final Thoughts
The permission system is the boundary between “the agent can do anything” and “the agent can do this one thing,” and the record of that decision is a config file that carries the secret. That is defensible if the file behaves like a secret. Here it behaves like source code: written into the repo, committed by the agent’s own commits, outliving the command by hundreds of days.
Anthropic’s instinct, secret scanning inside the agent, is the right shape at the wrong layer. Scanning catches the value after it is written. The durable fix is duller: write the ignore rule into the repository, warn when the file is tracked, and stop putting literal secrets in rules. Until then, your credentials are protected by a line in a git config on one laptop, and that laptop is not where your repository lives.
Thanks for reading.