When Claude Code asks to run a shell command and you pick "Yes, and don't ask again", it writes an allow rule into .claude/settings.local.json, inside the project. That is the designed behavior, documented in the permissions guide. The problem Apiiro quantified this week is what that rule contains when the command you approved had a secret typed into it.
Researchers Shmulik Cohen and Matan Giladi published the findings on November 5, and The Hacker News ran the English write-up today. The short version: they found 104,185 public copies of that one file on GitHub, and 1,122 of them hold at least one secret.
What the feature actually writes
The saved rule covers exactly what the prompt offered. Sometimes that is a pattern, like npm test *. Sometimes it is one exact command. When it is an exact command, everything you typed gets saved word for word, in plain JSON:
{
"permissions": {
"allow": [
"Bash(npm test *)",
"Bash(PGPASSWORD=hunter2 psql -h db-prod.internal -U app -c 'select 1')"
]
}
}
The first rule is harmless. The second one is a database password sitting in a file that half the internet's developers now know to look for. Apiiro reproduced the behavior with the current models, Opus 5.5 and Fable 5.1, so this is not a quirk of one model generation. The permission remembers the command, and the command contained the secret.
The numbers behind the headline
Apiiro collected public GitHub data between November 1 and 3. From those 104,185 copies across 103,617 repositories, about 1.1 percent (1,122 files) held at least one secret, 1,379 distinct values in total:
- 379 database passwords, the largest single category
- 43 SSH logins for publicly reachable servers
- 42 tokens for GitHub, GitLab and npm
- 23 AWS keys, plus 122 Supabase service-role keys
- owners spanning 63 organizations, mostly software companies
The strong cases they cite include a database vendor's own admin-level cloud key and the owner password of a medical-records database. As of Monday, every secret-bearing copy was still sitting on its repository's default branch, and in half of those repositories the file had been there for at least 217 days.
One honest limit worth keeping attached to this story: Apiiro classified secrets by format and context, with an AI reviewer agreeing on each candidate, and did not try any of them. One company they notified confirmed its exposed key had been live. So the right framing is "exposed and plausible", not "breached". iTWire's summary is careful about the same distinction.
Widen the scope to git history, the shared .claude/settings.json, and saved agent chat logs, and the total reaches 2,539 secrets across 1,994 repositories.
Why deleting the file fixes nothing
Removing the file from the project does not undo the leak. Older versions stay in git history, and forks keep their copy. Apiiro found 734 more secrets in the history of 509 repositories whose current copy is clean, usually because somebody deleted the file and called it done. Another 211 forks of 74 repositories carry the same secrets as the originals.
You can rewrite your own history with git-filter-repo or BFG. You cannot rewrite the forks. That asymmetry is why the researchers insist on the order: rotate first, clean up second. Rotation takes minutes and kills the credential everywhere at once. Cleanup takes days and never fully completes.
Claude commits the file itself
Claude Code does try to keep the file out of git. The first time it writes the file inside a git repository, it adds the path to your global git excludes, so git on that machine ignores it everywhere. But that protection lives in one user's git config on one computer. It never gets added to the project's own .gitignore, it does not travel with the repo, and it does nothing about a file that is already tracked.
And in 539 of the 1,105 repositories Apiiro traced, commits listing Claude as co-author were the ones that changed the settings file. The agent writes the secret down, then the agent commits the file, apparently past the exclude. I find that the most uncomfortable number in the whole report.
Why your secret scanner stays quiet
Apiiro ran gitleaks and TruffleHog over every copy they found, with the tools' verification step disabled. gitleaks flagged 564 of the 1,122 files, TruffleHog flagged 643, and 215 were missed by both. The misses are mostly passwords sitting inside ordinary commands. A password after PGPASSWORD=, sshpass -p, or mysql -p reads as one word among flags, and scanners look for known key formats, not words.
If your whole defense is "we run gitleaks in CI", roughly a third of these files sail through. I wrote earlier about config files holding secrets in the clear and about an agent beating secret scanners by splitting a token across pieces. This is the same class of gap, arriving from the permission layer instead of the prompt.
The cleanup order that matters
- Search your organization for both files,
.claude/settings.local.jsonand.claude/settings.json. The shared file had secrets in 298 of 76,011 public copies, and teams commit that one on purpose. Check git history in every repository, including repos where the file is gone today. - Rotate every password, token and key those files have ever held. Before any cleanup, not after.
- Untrack the local file and make the protection travel with the project:
git rm --cached .claude/settings.local.json
echo '.claude/settings.local.json' >> .gitignore
git log --all --oneline -- .claude/settings.local.json
That last command tells you whether history still holds copies you thought were deleted.
- Open the file on your own machine even if you never committed it, and delete every rule that contains a secret. GitHub is only the loudest leak path. The same plain JSON ends up inside npm packages, Docker images, zip archives and synced folders.
Rules that prevent the next one
Two habits cover most of the exposure.
Keep secrets out of the command itself. Refer to them as variables the shell resolves, so the saved rule holds the variable name and not the value:
psql "$DB_URL" -c 'select 1'
Approve patterns, not exact commands. Bash(npm run:*) covers every script in the package without ever recording what one specific deploy contained. Exact-command rules are the ones that archive your credentials for later sessions.
One more data point from the report: Apiiro compared the documentation of 16 coding agents, and Claude Code was the only one that saves the full approved command to a project file by default. Whatever agent you run, it is worth reading where its approval records land, the same way it is worth knowing what a cloned repo can set in your Claude Code settings.
The permission system did what you asked it to do. You approved a command that contained a secret, and it remembered the command. Treat settings.local.json as a credentials file from here on: rotate what it held, untrack it, and teach the team the two habits before the next don't-ask-again click does the remembering for you.
Top comments (0)