Your Agent's Saved Approvals Might Be Leaking Your Passwords

When Claude Code asks to run a command and you choose "Yes, and don't ask again", it saves a rule so it won't ask next time. If that command had a password in it, the password gets saved too, in plain text, in a file inside your project.
And this has already happened a lot. Apiiro searched public GitHub and found 1,379 secrets in 1,122 copies of that file (The Hacker News). Let's look at what gets saved, how to check your own projects, and how to approve commands so it doesn't happen again.
What gets saved
When you choose "Yes, and don't ask again" for a Bash command, Claude Code adds an allow rule to .claude/settings.local.json at the root of your repository. Sometimes that rule is a pattern, like npm run test *. Other times it's the exact command you approved.
Here's what that file can end up looking like:
{
"permissions": {
"allow": [
"Bash(npm run test *)",
"Bash(PGPASSWORD=hunter2 psql -h prod-db.example.com -U app -c \"select count(*) from users\")"
]
}
}
The first rule is fine. The second is the exact psql command the agent ran, and because the password was passed with PGPASSWORD=, it's now sitting in the file.
Any command with a secret typed into it gets saved the same way: sshpass -p, mysql -p, a bearer token in a curl header, or a connection string like postgres://app:hunter2@.... More than a third of the secrets Apiiro found open a database directly, including 379 database passwords. Others included SSH logins, AWS keys, and tokens for GitHub, GitLab and npm.
Why it ends up on GitHub
Claude Code does try to keep this file out of git. The first time it writes the file in a repository that doesn't already ignore it, it adds **/.claude/settings.local.json to your global git excludes file.
That ignore rule lives on your machine, though, not in the project. It's not in the repository's .gitignore, so it does nothing on a new laptop, inside a container, or once the file is already tracked.
Agents commit files too. In 539 of the 1,105 repositories Apiiro traced, the file had been changed by a commit with Claude listed as a co-author.
Check your projects
cd into a project and run these:
git ls-files .claude
git check-ignore -v .claude/settings.local.json
git log --all --oneline -- .claude/settings.local.json .claude/settings.json
git config --global core.excludesFile
The first shows whether anything in .claude is tracked by git. The second tells you whether the local settings file is ignored and which ignore file is responsible. If it prints nothing, nothing is ignoring it, and if it points at your global excludes, you're relying on the protection that only works on your machine.
The third lists every commit, on any branch, that has ever touched either settings file. This is the one that catches a secret you deleted months ago. The last one shows where your global excludes file lives (if it prints nothing, it's usually ~/.config/git/ignore).
Don't forget the shared .claude/settings.json either. Teams commit it on purpose, and Apiiro found secrets in 298 of 76,011 public copies, mostly in allow rules. If you've got a GitHub organisation, search it for both file names.
Claude Code's file is the one Apiiro counted, but other agents keep their own folders (.cursor/, .gemini/, .codex/), so I'd give those the same check.
Rotate first, then clean up
Deleting the file feels like the fix, but older versions stay in your git history and in any forks. Apiiro found 734 secrets that only existed in history, and 211 forks carrying the same secrets as the originals. So work through it in this order.
Rotate every secret
Change every password, token and key the file has ever held, including any you only find in git log. If the repository is public, assume they've been seen.
This is the annoying part, especially for a file that's been around a while and might have held secrets you've forgotten about. But it's the only step that stops a leaked secret from working.
Remove the rules
Open .claude/settings.local.json and delete every rule with a secret in it. This grep (from LATENT) catches the common shapes:
grep -nE 'PGPASSWORD=|sshpass -p|mysql .*-p|[Bb]earer |://[^/:@ ]+:[^@ ]+@' .claude/settings.local.json .claude/settings.jsonNo results doesn't mean the file is clean, though. A password passed any other way won't match, so read through the file yourself.
Stop tracking the file
Remove the file from git, add it to the project's own .gitignore, and commit both changes:
git rm --cached .claude/settings.local.json
echo ".claude/settings.local.json" >> .gitignore
git add .gitignore
git commit -m "Stop tracking local Claude Code settings"
The --cached flag removes the file from git but leaves your copy on disk. And because the ignore rule is now in the repository, it applies on every machine. Neither of these touches your history, forks or anyone else's clone.
Rewrite history last
If you need old versions out of your history, follow GitHub's guide to removing sensitive data from a repository. Rotate before you start, because rewriting history does nothing for forks and clones that already have the secret.
Approve commands differently
The simplest change happens at the prompt itself. If the command has a password or token in it, choose plain "Yes" so it runs once and nothing gets saved.
Better still, keep secrets in environment variables and have the agent reference them, so they never get typed into the command:
psql "$DATABASE_URL" -c "select count(*) from users"If that command gets saved as a rule, the rule holds $DATABASE_URL, not the password.
When the prompt offers to save a pattern, check how broad it is before you accept. Bash(npm run test *) only allows your test script, but Bash(npm *) allows any npm command, including npm publish.
You can also write the allow list yourself, so it only holds commands you've decided are safe. Here's a small one in .claude/settings.json:
{
"permissions": {
"allow": [
"Bash(npm run test *)",
"Bash(npm run lint)",
"Bash(git diff *)"
]
}
}
For sessions you leave running on their own, Claude Code 2.1.319 added permissions.denyInsteadOfPrompting. Set it to true in ~/.claude/settings.json (a project's settings can't turn it on), and anything that would have shown a prompt gets denied instead, so there's no "don't ask again" to click. It also denies file edits that would prompt and stops the agent asking you questions, so I'd keep it for unattended runs.
None of this cleans up rules you've already saved. The Claude Code team has said it's looking into ways to stop secrets being saved (per LATENT), but as of 2.1.322 there's no changelog entry that does it.
Catch the next one before it's committed
Ordinary secret scanners aren't great at this. They mostly look for known key formats, and a password inside a command looks like any other word. Apiiro ran gitleaks and TruffleHog over the 1,122 files, and 215 of them (19.2%, by LATENT's maths) weren't flagged by either.
If you've got a pre-commit hook or a CI check, make sure it has rules for PGPASSWORD=, sshpass -p, mysql -p and passwords in connection strings. Our Ship Gate skill is another option. It has your agent read the staged diff before you commit and flag anything that looks like a credential, including connection strings with a password in them.
GitHub isn't the only place the file can leak, either. .claude/ is just a folder in your project, so it can end up in an npm package, a Docker image or a zip you send someone. Add it to .npmignore and .dockerignore where they apply, and treat the folder the same way you'd treat .env.
If you've ever clicked "don't ask again" on a command with a password in it, I'd start by running those git commands across your projects. It only takes a couple of minutes, and you'll know straight away whether you've got anything to rotate.