DEV Community

Leo
Leo

Posted on Originally published at cicd.deployment.to

Claude Code's 'Don't ask again' file is leaking tokens into Git repos

When a developer picks "Yes, and don't ask again" in Claude Code, the exact shell command is saved to .claude/settings.local.json in the project folder. If that command carried a token, the token now sits in a plain file one git add . away from a remote. GitLab published a guide on November 11 covering prevention, exposure assessment and cleanup.

The problem is already measurable. In early November 2026, Apiiro reported 104,185 copies of the file in public repositories, per the GitLab post. Of those, 1,122 held at least one secret. GitLab adds that hundreds more secrets survived only in history, in files that had since been deleted. GitLab says its own infrastructure secrets are protected.

How the file gets committed

The file is an allowlist. Claude Code records approved commands under permissions.allow so the agent can rerun them without asking again. An entry for a test runner is harmless. A curl call with a token in the header is a stored credential.

{
  "permissions": {
    "allow": [
      "Bash(npm test:*)",
      "Bash(curl --header \"PRIVATE-TOKEN: <your-token>\" ...)"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Claude Code does try to keep the file out of version control. It does that through the developer's personal global excludes, not the project's .gitignore. A global excludes file is Git configuration on one machine. It applies on that machine only and does not travel with the repository. A rebuilt laptop or a fresh dev container starts without it, and the repository has no rule of its own to fall back on.

Four controls, set once

GitLab's guide stacks four prevention layers. Per the post, each is a setting a group owner changes once.

  1. A .gitignore entry for .claude/settings.local.json, added to repositories and to group file templates. All tiers.
  2. A push rule that rejects the file by path, using the regex (^|/)\.claude/settings\.local\.json$. Premium and Ultimate.
  3. Secret Push Protection, which rejects pushes containing known token formats regardless of filename. Ultimate.
  4. A scan execution policy that runs GitLab Secret Detection in every project. Setting SECRET_DETECTION_HISTORIC_SCAN: 'true' extends the scan to history. Ultimate.

Two caveats from the post decide the rollout order. Push rules check only newly added files, so a file that is already tracked passes. Clean up first, then turn the rule on. Group-level push rules also apply only to projects created afterwards. Existing projects need the rule configured one by one, which is toil worth scripting.

Layers 1 and 2 match a path. Layers 3 and 4 match content, and they look for known token formats. A credential in a format the scanner does not recognise gets past both content checks. The path rules are what catch it.

Checking for exposure

The guide gives three routes, depending on tier.

With GitLab Duo, Agentic Chat can query commit history across projects and report current and deleted copies of the file along with the secret types involved.

Without Duo, Premium and Ultimate customers can use code search. With Zoekt the query is file:\.claude/settings\.local\.json$. With Elasticsearch it is filename:settings.local.json path:.claude.

On any tier, the Commits API works:

GET /api/v4/projects/<id>/repository/commits?path=.claude/settings.local.json&all=true&follow=false
Enter fullscreen mode Exit fullscreen mode

Keep follow=false. The post warns that without it, the call can return empty results for repositories with more than one page of commits. In an audit script, an empty result looks like a clean repository, which is the worst way for it to fail.

Cleanup order

Rotate first. GitLab's guide says to rotate every exposed secret at its source provider before touching any files. Deleting the file does nothing for a token that was already pushed.

Then remove the file and add the .gitignore entry. Then purge history under Settings > Repository > Repository maintenance > Remove blobs, run housekeeping, and allow about 30 minutes for objects to be pruned. Forks keep their own copies and need the same assessment and cleanup separately.

Until each repository has its own ignore rule, a clone is only as safe as the Git config on the machine that cloned it.

Top comments (1)

Collapse
 
max_quimby profile image
Max Quimby •

The root cause here deserves underlining because it's so easy to miss: the protection lives in the developer's global git excludes, which don't travel with the repo — so a rebuilt laptop or a fresh dev container starts unprotected, and the repository has no rule of its own to fall back on. That one detail is the entire bug.

The deeper problem is that the allowlist stores the literal command, so any secret passed inline (a curl with a token header) gets persisted as a side effect of clicking "don't ask again." The durable habit is to never put the secret in the command at all — reference an env var or a secrets manager — so even if the allowlist entry leaks, it leaks $GITLAB_TOKEN, not the value. Worth flagging on the four GitLab layers too: 3 and 4 only catch known token formats, so a custom or internal credential format sails past content scanning entirely. The path rule is the only thing that catches that one, which is a good argument for keeping layers 1–2 even if you have Ultimate.