Somewhere in a public GitHub repository, a production database password is sitting in a JSON file. It has been sitting there for 217 days. Nobody wrote it on purpose. It got there because a developer asked an AI coding agent to check why staging login was failing, the agent proposed the obvious command, and the developer clicked "Yes, and don't ask again."
That click is the whole story.
On 6 November, software security firm Apiiro published research showing that this exact pattern has left 2,539 real secrets exposed across 1,994 public GitHub repositories. Among them: a database vendor's own admin cloud key with no expiry, a production Azure Postgres password, a Square production access token, and the owner password of a medical-records database. Sixty-three organizations, mostly software companies, have at least one live credential in a public allowlist. One company confirmed its key was still working when researchers called.
This is not a model problem. It is a product-behavior problem, and it is happening to teams that thought they were being careful.
The mechanism is three words long
Claude Code asks before it runs a shell command. That's the guardrail, and it's a reasonable one. But the prompt offers an option: "Yes, and don't ask again." Choose it, and Claude Code writes a permission rule into .claude/settings.local.json inside the project folder. The rule is the command string, word for word.
If the command was PGPASSWORD=hunter2 psql -h prod-db.internal -U admin, then the rule is PGPASSWORD=hunter2 psql -h prod-db.internal -U admin. The agent didn't decide to save your password. It saved your command, and your command contained your password.
Apiiro reproduced this with Anthropic's newest models, Opus 5.5 and Fable 5.1. It is the surrounding tool, not the reasoning layer. Of sixteen coding agents whose documentation the researchers compared, Claude Code is the only one confirmed to store the full approved command in a project file by default.
The protection exists, but it doesn't travel
Here's where it gets interesting, and where most write-ups stop one step too early.
Claude Code does try to keep that file out of git. The first time it writes the file inside a repository, it adds the file to your global git exclude list. On your machine, in your git config, git now ignores that path in every repository you touch. That is genuinely thoughtful engineering.
It also doesn't survive contact with reality.
Global excludes live in one user's config on one laptop. The moment the file is already committed, the exclude does nothing. It doesn't protect a clone on a colleague's machine, a folder that becomes a repo after the fact, or a fork. And Claude Code never adds the path to the project's own .gitignore, so the protection is not part of the repository at all. It never ships with the code.
Worse: in 539 of the 1,105 repos with a confirmed secret, a commit co-authored by Claude is the one that changed the file. The agent, doing what agents do, committed the work. The work included its own permission list.
What the numbers actually say
Apiiro scanned 104,185 public copies of .claude/settings.local.json across 103,617 repositories. Of those, 1,122 files (roughly 1.1%) hold a real secret: 1,379 distinct credentials. The breakdown is worth sitting with:
- 379 database passwords and 122 Supabase service-role keys, which open a database directly
- 103 Supabase account tokens, which control every project in that account
- 43 SSH logins to public servers, several of them root via
sshpass - 42 GitHub, GitLab and npm tokens, 23 AWS keys, 6 private keys
More than a third of the exposed credentials open a database. Those are not tokens you rotate on a quarterly cycle. Those are the crown jewels, sitting in a file with a friendly name.
Size correlates brutally with risk: one in ten files over 8 KB contains a real secret, against fewer than one in five hundred under 500 bytes. The longer a developer has been clicking "don't ask again," the heavier the file gets. The median exposed file was first committed 217 days before discovery. In 118 repositories, it's been over a year.
Deleting the file doesn't fix it either. Earlier versions hold another 734 secrets across 509 repositories that appear in no current copy, usually because someone did eventually delete the file. Forks make it worse: 211 forks of 74 repositories carry the same secrets as the original. This is the standard git property that surprises everyone the first time: a secret committed is a secret permanent, and a secret permanent in a public repo is a secret published.
Your secret scanner probably missed it
Here's the part security teams need to hear. Apiiro ran gitleaks and TruffleHog over all 104,185 files. Of the 1,122 that held a real secret, gitleaks flagged 564 and TruffleHog flagged 643. 215 were flagged by neither.
The blind spot is passwords inside commands. Scanners are tuned for key formats: a provider prefix, a long random string, an entropy threshold. A password after PGPASSWORD=, sshpass -p, mysql -p, or in a bearer header inside a curl call is just an ordinary word in an ordinary command line. It looks like shell, because it is shell.
And this pattern isn't confined to one file. The same research found secrets in:
- the shared
.claude/settings.jsonteams commit on purpose (298 of 76,011 files) - agent chat transcripts from
.specstory, Aider and Cursor exports (120 files in 87 repos) - agent memory files like
MEMORY.md(9 repos) - handoff and deploy notes (33 files, a third of them written by the agent itself)
- VS Code Copilot auto-approve settings (9 repos)
The habit that leaks is identical everywhere: a person types a secret into a command, and an agent dutifully records the command.
What to do, in order
Rotate first. Clean up second. That ordering is not a preference; it's the only sequence that works, because the moment a credential touches a public commit it belongs to whoever read it, and history remembers.
- Search your org for the file. Query GitHub for
.claude/settings.local.jsonand.claude/settings.jsonacross every repo, and check git history for them even where the file is gone today. - Rotate everything the file has ever held — passwords, tokens, keys, connection strings — before you start deleting.
- Untrack and ignore.
git rm --cached .claude/settings.local.jsonand add it to the repository's own.gitignore, not just your global one. - Open the file on your own laptop, committed or not, and delete every rule containing a secret. Most of these files never touched GitHub and are still plain text on disk.
- Stop putting secrets in commands. Reference
$DB_URLfrom a secrets manager so the saved rule records a variable name, not a credential. - Approve patterns, not commands.
Bash(npm run:*)is a rule.npm run deploy --token abc123is a leak.
For security teams, four additions: treat .claude/, .cursor/, .gemini/, .specstory/ and agent memory files as secret-bearing paths on par with .env; add them to pre-commit and CI scanning including history; put .claude/settings.local.json in your org's .gitignore template; and triage credentials paired with a public host address first, because those are the ones that grant access from anywhere.
The takeaway we keep repeating
The lesson here isn't "AI agents are dangerous." Anthropic's permission model is a sensible one, and Claude Code's global exclude shows someone thought about this specific failure. The lesson is that convenience settings quietly become storage. Every "don't ask again" is a record. Every record has a path. Every path eventually ends up in a repository, an archive, a Docker layer, or a synced folder.
If your team has been shipping with AI coding agents for a year, you almost certainly have a .claude/ directory that predates any policy you've written since. Go read it. Not the repository version — the one on the laptop in front of you. Then check what the scanners said about it, and assume they said nothing.
The command you approved once is still holding your password. It will keep holding it until you rotate it.