TL;DR
- 31,088 secrets, 5,343 valid: GitGuardian found 31,088 distinct secrets in Claude Code’s
.claude/settings.local.jsonon public GitHub, across 23,276 repositories, and 5,343 of them worked when we detected them. - “Yes, and don’t ask again” writes the key: approving a command permanently makes Claude Code save an allow rule that is the command string itself, API key included, in the project’s settings file.
- One valid token in 559 Docker image tags:
COPY . .ships the working tree, not the git index, so 613 secrets, 99 valid, reached public Docker Hub images; one valid Cloudflare API token appears in 559 tags. - 92% drop from the April 2026 peak: monthly leaks in
settings.local.jsonpeaked at 12,535 occurrences in April 2026, then fell to 959 by October, and we do not yet know what changed.
I have been doing research for a long time, and a good sign that you are working on something worth doing is that other researchers are working on the same topic. It can feel frustrating at first, but it is great. This one first occurred to me in early 2026, while I was doing research on n8n and kept finding n8n API keys inside Claude Code settings files. On November 5, Apiiro published their analysis of .claude/settings.local.json. Here are the results I had planned to release in the State of Secrets Sprawl 2027, refreshed to this week, with what only continuous monitoring can see. Every version of the file we saw pushed, whether the credentials worked, who pushed them, and where the file travels after GitHub.
The mechanism, in one paragraph
Take the case that put me on this trail. You ask Claude Code to list the workflows on your n8n instance. It proposes:
curl -H "X-N8N-API-KEY: n8n_api_9f3c…e21a" https://n8n.example.internal/api/v1/workflows
Claude Code asks before running a shell command. You answer "Yes, and don't ask again" because you will be calling this API all afternoon. It runs the command and writes an allow rule into .claude/settings.local.json in the project folder. The rule is the command string itself, API key included. Claude Code keeps that file out of git on your machine, but nothing in the repository does, so on another machine, in an older clone, or in a file that was already tracked, it gets committed with everything else.
What continuous monitoring adds
GitGuardian scans every public commit as it is pushed. Apiiro's code search saw the files on default branches on November 1 to 3, plus a history pass over 43,120 repositories and 211 forks. We saw each version as it was pushed, including the ones since deleted, rewritten or force-pushed away. Public GitHub, January 2025 through November 7, 2026:
Apiiro counted 1,379 secrets in current copies of settings.local.json plus 734 in history. We count 31,088. The two numbers answer different questions. They asked what is exposed today, with a conservative filter and no validity check. We asked what was ever pushed, and we counted every credential our automatic check found valid at detection. Both numbers are right. The second one is the number of credentials that should have been rotated.
The credentials were valid when we checked
Apiiro chose not to test validity. GitGuardian runs an automatic, read-only check on every public detection; nothing was replayed for this post. Where that check returned a verdict, 67% of occurrences in settings.local.json and 57% in settings.json were valid. Over everything we found, 5,343 of 31,088 distinct secrets (17%) worked at detection, in a file that is supposed to never leave the laptop. Detectors without a checker are never counted as valid, so these are floors.
Bearer tokens in curl calls make up the volume. Database credentials are the ones that work. settings.local.json alone holds 1,955 distinct valid Postgres connection strings (5,076 valid occurrences), 292 valid SSH passwords, 116 valid MongoDB URIs, 941 valid Google API keys, 355 valid Telegram bot tokens, 142 valid Resend keys and 186 valid DeepSeek keys. By family, database identifiers are the largest live bucket with 2,280 distinct valid secrets, then cloud provider tokens with 971, then AI tokens with 583.
Which commands carry the secret
We located each secret inside the file to see which rule carried it, on a sample of about 3,000 occurrences. Every secret we could place sat in an allow rule or an env block. None sat in deny:
The volume is curl. The valid credentials are database and SSH.
The same split explains the scanner gap. Apiiro measured that gitleaks and TruffleHog together miss one in five leaking files. In our data, generic detectors (passwords, high-entropy strings, JWTs, bearer tokens, username and password pairs) produce 63% of all occurrences in settings.local.json: 44,755 of 71,525. A password after PGPASSWORD= has no prefix to match. A scanner that only knows provider key formats is blind to most of this file.
A different file, a different leak
.claude/settings.json, the shared project file teams commit on purpose, leaks for a second reason. A quarter of its leaking versions contain an env block pointing Claude Code at a third-party Anthropic-compatible gateway: ANTHROPIC_BASE_URL plus ANTHROPIC_AUTH_TOKEN. The hosts are DeepSeek, OpenRouter, Z.ai, MiniMax, Alibaba's DashScope, Kimi, Moonshot, and a long tail of resellers. The credential that leaks is the gateway's: 205 valid DeepSeek keys, 62 valid OpenRouter keys, 50 valid MiniMax keys in settings.json. Real Anthropic API keys are rare in both files: 139 distinct in settings.local.json, 28 of them valid.
What about the other agents
Apiiro compared 16 agents from their documentation and found Claude Code to be the only one that writes the full approved command to a project file by default. Our data agrees. We counted the config files of 30 agents on public GitHub. All of them leak, and all of them leak the same way: MCP server definitions with API keys in env blocks. Judged by the mix of detector types per file, only .claude/settings.local.json is dominated by credentials typed into commands, which is what a persisted approval looks like.
Two more Claude Code files deserve a line. CLAUDE.md holds 6,155 distinct secrets, 2,537 of them valid, the highest validity rate of any file we looked at. Developers paste real connection strings into the instructions they give the agent. And .claude/.credentials.json, the agent's own OAuth login to Anthropic, which lives in the home directory, appears in 371 repositories with 1,366 distinct tokens: whole home directories were committed. Codex has the same problem with .codex/auth.json: 1,064 tokens in 536 repos.
The curve
A snapshot shows persistence. Apiiro found the median leaking file had sat on its default branch for 217 days. Continuous monitoring shows the inflow. Leaks in settings.local.json start in June 2025 with 256 occurrences, pass 3,000 a month by October, and reach 12,535 in April 2026. Then they fall: 5,276 in June, 1,916 in August, 959 in October. A 92% drop from the peak. settings.json follows the same shape with a one-month lag, peaking in May at 3,084 and down 84% by October. I do not know what changed in the spring. Candidates: a Claude Code change to the permission prompt or its ignore handling, adoption flattening, GitHub push protection, or our own detector coverage. We have not finished ruling out changes on our side; the rest is a question for Anthropic.
Beyond GitHub: Docker Hub
Apiiro lists Docker images as a possible exit path. We measured it.

Public Docker Hub images carry 109,284 occurrences of secrets in .claude/settings.local.json and 29,613 in .claude/settings.json, across 543 image repositories and 7,625 tags. 72% sit under /app, /workspace or /usr/src: a repo copied wholesale into an image with a COPY . . instruction. COPY takes the working tree, not the git index, so a file git ignores still ships unless .dockerignore lists .claude/. The distinct count is 613 secrets, 99 of them valid, because one committed file is rebuilt into every tag. One valid Cloudflare API token appears in 559 tags of a single image. One database password appears in 643 tags across seven image repositories. Deleting the file from the repo does nothing for the images already pushed.
Who is affected
We classified the git author's email domain for every occurrence in both files. Personal mail providers account for 15,993 distinct secrets, 4,270 of them valid. GitHub noreply addresses add 3,084 (773 valid). 3,338 corporate or custom domains hold 5,924 secrets (1,499 valid), and 266 academic domains hold 426 (103 valid).
Matching the corporate domains against company records gives a sector view for 417 organizations. Education and research lead with 172 institutions and 75 valid credentials, followed by software and IT services with 141 companies and 69 valid credentials. Together they are three-quarters of the matched set. The rest is a long tail: telecom (15), retail (13), finance and insurance (11), media and marketing (10), industry and construction (10), healthcare (5, with 4 valid credentials), government and defense (4). The sample is biased toward companies we have records for, so read the ranking, not the absolute counts.
In 263 repositories, the leaking commit carries Claude's noreply address as its git author, which means Claude Code or its GitHub action made the commit on the user's behalf: 406 distinct secrets, 107 of them valid. Apiiro saw the same pattern as a co-author trailer in 539 of 1,105 repositories.
How to remove secrets from Claude Code settings.local.json
Deleting settings.local.json does not remove its secrets, because history, forks, and Docker Hub images keep copies, so every fix starts with rotation.
For developers
Rotate everything the file ever held first; history, forks, and images keep copies, and rewriting history does not reach them. Then add it to the repo's own .gitignore and .dockerignore. Open the file, or run /permissions, and delete every rule containing a secret. Reference secrets as $N8N_API_KEY in commands: the rule stores the literal string, so it records the name, not the value, though the value still reaches the transcript if the shell echoes it. Prefer a narrow prefix such as Bash(curl -H "X-N8N-API-KEY: $N8N_API_KEY" https://n8n.example.internal:*) over Bash(curl:*), which approves any curl, including one written by a prompt injection.
For security teams
Treat .claude/, .cursor/, .gemini/, .codex/, .mcp.json and CLAUDE.md as secret-bearing, like .env. Scan them in pre-commit, in CI, in history, and in your container registry. Make sure the scanner catches passwords inside commands, not only provider formats.
Anthropic
The fix is in the tool. Never persist an exact-match rule whose command contains a VAR=value prefix, a header value, URL userinfo or a password flag; offer a session-only approval instead. You can also talk to us.
What Anthropic said
Boris Cherny, Anthropic's Claude Code lead, replied on Hacker News (as reported by iTnews) that the team is looking into the problem, that Claude Code has not historically scrubbed secrets, and that auto mode and built-in secret scanning are being prototyped but may not fully solve it. He also said Claude Code gitignores the file when it creates it, so the exposed copies were likely created manually and committed, and recommended auto mode over permission rules together with the secret scanning of GitHub and other hosts.
Two of our findings are related to that. The content of the files was not typed by hand: in the 3,000 occurrences we inspected, every secret sat in an allow rule of the exact shape the permission prompt writes. 63% of what we found are passwords and tokens with no provider format, and Docker Hub kept the ones GitHub revoked.
Stopping it before the prompt
A secret that never reaches the permission prompt never reaches the file. GitGuardian AI Hooks plug ggshield into the agent's own hook system. For Claude Code, the pre-tool-use hook scans every shell command, file read, and MCP call before it executes. The n8n curl above is blocked with a message naming the secret, so there is nothing to approve and nothing to persist. A post-tool-use hook withholds command output that contains a secret, so a leaked key in a response never reaches the model or the transcript. Install with ggshield machine setup; check with ggshield machine doctor. The same hooks cover Cursor, Codex, Copilot CLI, VS Code, and others. Two limits. The hook fails open when ggshield is not authenticated. Blocking a command does not fix the habit of pasting secrets into commands.
FAQ
Why does Claude Code save secrets in .claude/settings.local.json?
When Claude Code asks to run a shell command and you answer "Yes, and don't ask again," it writes an allow rule into .claude/settings.local.json in the project folder. The rule is the command string itself, API key included.
Isn't settings.local.json kept out of git?
Claude Code keeps the file out of git on your machine, but nothing in the repository does. On another machine, in an older clone, or when the file was already tracked, it gets committed with everything else.
How many secrets are exposed in Claude Code settings files on public GitHub?
From January 2025 through Nov. 7, 2026, GitGuardian found 31,088 distinct secrets in .claude/settings.local.json across 23,276 repositories, and 8,271 in .claude/settings.json across 6,839 repositories. Of those, 5,343 and 1,283 respectively were valid at detection.
How do Claude Code secrets end up in Docker Hub images?
A repository copied into an image with a COPY . . instruction takes the working tree, not the git index, so a file git ignores still ships unless .dockerignore lists .claude/. Public Docker Hub images carry these files across 543 image repositories and 7,625 tags, holding 613 distinct secrets, 99 of them valid. Deleting the file from the repo does nothing for images already pushed.
How do I remove secrets from Claude Code settings files?
Rotate everything the file ever held first, because history, forks, and images keep copies, and rewriting history does not reach them. Then add the file to the repo's own .gitignore and .dockerignore, and delete every rule containing a secret by opening the file or running /permissions. Going forward, reference secrets as variables such as $N8N_API_KEY so the rule stores the name, not the value, and prefer narrow command prefixes over broad ones like Bash(curl:*).





