DEV Community

TECHDEV
TECHDEV

Posted on Originally published at github.com

Your AI Coding Agent Saved Your Password to Disk. I Built the Tool That Finds It.

One click. "Yes, and don't ask again." Your agent runs the database command, and the password you typed rides along into a file you will probably never open: .claude/settings.local.json, in plaintext, as a permanent allow rule.

That is not a hypothetical. Apiiro's research (published 2026-11-05, files collected 2026-11-01 to 2026-11-03) scanned 104,185 public copies of that file and classified 1,122 (1.1%) as containing real secrets — 1,379 distinct values. Counting git history, shared settings and agent transcripts, the total is 2,539 secrets across 1,994 repositories and 63 organisations, including 379 database passwords, 122 Supabase service-role keys, 43 SSH logins, 42 GitHub/GitLab/npm tokens and 23 AWS keys. Credentials were not tested; one notified company confirmed its key was still live. The 2,539 total rests partly on an LLM reviewer's judgement — it is Apiiro's number, not mine.

The part that got me: the same research ran the two big generic scanners over those 1,122 files. Gitleaks flagged 564. TruffleHog flagged 643. 215 were flagged by neither — usually because the secret is a password typed inside a command, not a key in a recognizable format.

So I built persistguard: a zero-dependency TypeScript CLI that parses the allow list as a structure and catches exactly that shape.

# Find secret-bearing allow rules (exit 1 on findings, 0 when clean)
persistguard scan .

# Was a settings file ever committed? (exit 1 if tracked or ever committed)
persistguard history .

# Dry run: print what would change. Nothing is written.
persistguard clean .

# Rewrite the secrets out (backup .bak first; refuses tracked files
# without --force - rewriting a tracked file does not un-leak history).
persistguard clean --write .

# Install the pre-commit gate + repo .gitignore fix
persistguard hook install .
Enter fullscreen mode Exit fullscreen mode

A secret value is never printed anywhere — not in the table, not in --format json output, not in logs. Previews show at most the first 4 and last 2 characters of a token.

Here is the worked example from the repo (every credential in the fixture is synthetic and labelled FAKE):

$ persistguard scan examples/demo-project
.claude/settings.local.json (5 rule(s))
  [HIGH] credential-url  rule #1 (allow)  Bash(psql postgres://app:<REDACTED-BY-PERSISTGUARD>@db.internal:5432/app -c "select 1")
  [HIGH] openai-key  rule #2 (allow)  Bash(curl -H "Authorization: Bearer <REDACTED-BY-PERSISTGUARD>" https://api.example.com/health)
1 file(s), 5 rule(s), 2 finding(s) (2 HIGH, 0 MEDIUM)

$ persistguard clean --write --force examples/demo-project
persistguard clean --write: 2 finding(s) redacted in 1 file(s)
Rotation checklist - redaction does not un-leak history:
  - credential-url: rotate/revoke this credential
  - openai-key: rotate/revoke this credential
  Delete the .bak backup after rotating - it still holds the original secrets

$ rm examples/demo-project/.claude/settings.local.json.bak
$ persistguard scan examples/demo-project
1 file(s), 5 rule(s), 0 finding(s) (0 HIGH, 0 MEDIUM)
Enter fullscreen mode Exit fullscreen mode

The rm step is part of the flow, not cleanup: the .bak backup holds the original secrets in plaintext, so scan keeps failing until it is deleted. The hook's .gitignore fix also covers .claude/*.bak, so a backup can never be committed by accident — but only deleting it removes the secret from the disk.

It also closes the gap Claude Code's own protection leaves open. That protection adds the file to the user's global git excludes on that one machine. It never lands in the repo's .gitignore, so clones, other laptops and already-committed copies stay exposed — and it cannot protect a file that is already tracked. history checks git ls-files plus git log --all, so even a file that was committed and later deleted still shows up.

Where rivals beat us

Gitleaks and TruffleHog scan every file in a repo and its full history, with hundreds of detectors and verified-secret checks; persistguard only looks at agent persistence files and only detects secret-shaped values inside allow rules. GitGuardian remediates at push time across an org; persistguard is local-only, one machine, one repo. Claude Code's global-excludes protection is automatic and needs no install; persistguard needs the user to run it (or install the hook). What persistguard does that none of them do: parse the allow list as a structure, catch the password-inside-a-command shape, rewrite the persisted rule, and write the repo-level .gitignore fix.

Honest limits

Detectors are heuristic. A password that looks like an ordinary word next to no host will be missed, and a high-entropy string that is not a secret can be flagged. clean rewrites the file on disk only — if the file was ever committed, the secret is in git history: rotate it and rewrite history with git filter-repo. This tool does not do that, and it refuses to rewrite a tracked file without an explicit --force. I also ran it over 127 real allow/ask/deny rules from two real project settings files: 0 findings, 0 false positives — but 0 findings also means no real secret was caught there yet, so treat the detectors as a net with known holes, not a guarantee.

Install from source for now (npm is not published yet):

git clone https://github.com/techdevfun/persistguard.git
cd persistguard
npm ci
npm run build
npm link
Enter fullscreen mode Exit fullscreen mode

Repo: https://github.com/techdevfun/persistguard — MIT. If your agent has ever asked you to click "don't ask again", run scan before your next commit. What did it find?

Top comments (0)