AI coding agents are now one of the fastest-growing sources of leaked credentials on GitHub. Agents like Claude Code and Cursor keep per-project settings in a dotfile such as .claude/settings.local.json. That file picks up secrets during normal use, and a developer commits it with the rest of the project, straight to a public repository.
As Ars Technica reported, researchers scanning public GitHub found tens of thousands of exposed secrets across more than a hundred thousand public copies of these files, many still live when discovered. Uninstalling the agent does nothing. The credential stays in the repository and its history until someone rotates it and rewrites the git history.
TL;DR
- What leaks: agent settings files (
.claude/settings.local.jsonand friends) collect credentials two ways: inside approved command permissions, and inside MCP serverenvblocks. - Why it spreads: the file looks like harmless editor config, so it gets committed and pushed to public repos without a second look.
- Find it:
xgrep scan --category secrets .flags the credentials by default, including bare passwords under JSON-quoted keys.--validateconfirms which keys are still live, and--historyrecovers ones already deleted from the working tree. - Block it: the
mondoohq/actions/xgrepGitHub Action runs a diff-aware scan on every pull request and fails the build before the secret merges. - If you've already leaked: revoke and rotate the credential,
.gitignorethe file, and scan the full history. A deleted secret is still a compromised one.
How the secrets get there
There are two shapes, and both matter. A scanner that only knows provider key formats will miss one of them.
1. "Don't ask again" permission rules. When a user approves a command with "yes, and don't ask again," the exact command is written into the settings file's allow-list. If it carried a credential, like a curl with an Authorization header, that credential now sits on disk verbatim:
JSON"permissions": {"allow": ["Bash(curl -H \"Authorization: Bearer <prod-deploy-token>\" https://api.internal.example/deploy)"]}
2. MCP server env blocks. Agents launch MCP (Model Context Protocol) servers with environment variables, and the settings file stores them in plaintext. That is where API keys and database passwords end up:
JSON"env": {"GITHUB_TOKEN": "ghp_<redacted>","STRIPE_SECRET_KEY": "sk_live_<redacted>","AZURE_STORAGE_CONNECTION_STRING": "...;AccountKey=<redacted>;...","DATABASE_PASSWORD": "<redacted>"}
These do not carry the same weight. The Azure Storage account key is the most dangerous: it grants full, unscopeable control of every blob, file, queue, and table in the account. A GitHub or Stripe key has a distinctive prefix any scanner recognizes. The database password is the one that slips through, because it has no prefix to match, and in JSON its key is quoted ("DATABASE_PASSWORD": "…"), exactly where a lot of generic password detection breaks.
Find them with xgrep
xgrep is a Semgrep-compatible scanner with 270+ secret detectors built in. Secret scanning is on by default, so pointing it at a repository finds these files with no configuration:
Bashnpm install -g @mondoohq/xgrepxgrep scan --category secrets .
The xgrep docs cover install options for every platform and the full flag reference if you want to get started fast.
On a settings file carrying the two shapes above, xgrep reports every real credential and stays quiet on the near-misses:
![]()
Every real credential is caught and ranked, from the critical Azure key down to the medium provider tokens. The ${ANTHROPIC_API_KEY} interpolation and the your-token-here placeholder beside them are not flagged, because neither holds a credential value.
xgrep can also tell you which keys still work. The opt-in --validate flag checks each candidate against the issuing provider's API, and nothing else, confirming the live ones and promoting them to their real severity. That is why a GitHub or Stripe token stays at medium until the check runs: an unconfirmed token and a dead one look identical on disk. Validation tells you which ones to rotate first.
Bashxgrep scan --category secrets --validate .
A credential that was committed and later deleted is still in the repository's history, and still compromised. --history walks every commit's additions and recovers it, with provenance:
Bashxgrep scan --history --category secrets .
Know which one to fix first
Connect the repository to Mondoo and the same findings are scored and prioritized, so the one that can do the most damage sorts to the top. The leaked Azure Storage account key lands as mission-critical, with the exact file and line it came from:
![]()
Block the next one in CI
The mondoohq/actions/xgrep GitHub Action stops a leaked secret before it merges. It runs a diff-aware scan on every pull request, scoped to what the PR changed, and with fail-on set it blocks the merge on any credential:
YAML# .github/workflows/xgrep.ymlname: xgrepon:pull_request:push:branches: [main]permissions:contents: readsecurity-events: write # upload findings to the Security tabjobs:xgrep:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v4with:fetch-depth: 0 # history is needed for diff-aware PR scans- name: Scan with xgrepuses: mondoohq/actions/xgrep@v13.2.0with:path: .output-file: results.sariffail-on: error # block the PR on any finding- name: Upload SARIF to code scanningif: always() # don't lose findings if the scan step errorsuses: github/codeql-action/upload-sarif@v3with:sarif_file: results.sarifcategory: xgrep
A pull request that adds a .claude/settings.local.json with a live key now fails the check and cannot merge. If you already carry a backlog, start with fail-on: "off" so findings surface in the Security tab without breaking the build, then tighten to error once triaged. The action is self-contained: it installs xgrep from npm, scans, and writes a SARIF report, with no Mondoo account or token required.
Prefer the binary directly? xgrep ci --error does the same gating in any CI system: it auto-detects the provider, scans diff-aware against the PR base, and exits non-zero on findings.
If you've already committed one
Treat every secret you find as compromised. Rotation is not optional.
- Revoke and rotate the credential at its provider. A key pushed to a public repo should be considered captured, even if the commit was deleted minutes later.
- Stop committing the file. Add the agent's local settings to
.gitignore(.claude/settings.local.json, and the equivalents for other agents). Treat agent directories like.envfiles: local, never committed. - Scan the whole history, not just the tip.
xgrep scan --history --category secrets .finds the ones already removed from the working tree but still reachable in git history. - Move the value out of the file. Reference secrets at runtime from a secret manager or an environment variable the file never stores literally.
The settings files AI agents leave behind are now part of your attack surface. Scan them like any other source, and gate them in CI so the next leak stops at the pull request instead of on GitHub.


