Claude Code "Don't Ask Again": Check Your Repo
Don't ask again can save a password typed into a command to settings.local.json. Check if your repo leaked one, and rotate the key before you untrack it.
What Claude Code's "don't ask again" saves
When you answer "Yes, and don't ask again" to a Claude Code Bash prompt, Claude Code saves an allow rule in .claude/settings.local.json. If the command you approved had a password in it, that password can sit in the rule as plain text. The file is meant to stay on your machine. Even so, the security firm Apiiro found 1,122 public GitHub copies of it that held a real secret.
The audit below takes five git commands. If it finds a secret, rotate that secret before you do anything else.
This behavior is documented. The quotes here come from the Claude Code settings page, read on 9 November 2026. On that day the current release was 2.1.319, dated 6 November 2026 in the changelog. The page has no version stamp of its own. It says: "When Claude asks permission to run a Bash command and you choose "Yes, and don't ask again", Claude Code saves that permission approval here as an allow rule."
The rule is saved at the root of the git repository. It applies to every later session in that repository, including sessions started in subdirectories and in worktrees. Before v2.1.211, Claude Code kept the file in the directory where you started it, so an older repository can hold extra copies further down the tree.
Prefix rule or full command?
The text that goes into the rule depends on the prompt. The permissions docs show a Bash prompt that offers "Yes, and don't ask again for: npm test *", which saves a prefix rule. Apiiro reports that the exact command string was saved, embedded password included. It says it reproduced this with Opus 5.5 and Fable 5.1. Do not guess which form your own prompts produced. Read the file:
cat .claude/settings.local.jsonA rule such as "Bash(mysql -u app -pS3cretValue shop)" means the password is in the file. A compound command like git status && npm test saves a separate rule for each subcommand that needed approval, up to five rules. So a secret in any part of a chain can be saved on its own.
Why public copies exist: two views
Apiiro published its report on 5 November 2026. It updated the report on 8 November with Anthropic's response. The report counts public GitHub copies of both Claude Code settings files.
The data behind this chart
[
{
"label": "settings.local.json (personal)",
"public_copies": "104,185",
"with_real_secret": "1,122",
"percent_with_secret": "1.08"
},
{
"label": "settings.json (shared)",
"public_copies": "76,011",
"with_real_secret": "298",
"percent_with_secret": "0.39"
}
]Of the 104,185 public copies of settings.local.json, 1,122 held a real secret. That is about 1.08%. The percentage column is calculated from Apiiro's counts. Across every agent file it checked, Apiiro counted 2,539 distinct real secrets in 1,994 repositories. Most of those repositories belong to individual developers. Apiiro separately counts 63 affected organizations, from student groups to startups, software agencies and established vendors.
Anthropic's view: the file is ignored when Claude Code creates it. The settings page says: "The first time Claude Code writes the file in a git repository that doesn't already ignore it, it adds **/.claude/settings.local.json to your global git excludes file, so the file stays out of your commits in every repository." Boris Cherny of the Claude Code team wrote on Hacker News (HN) on 7 November: "We automatically gitignore it when Claude creates the file, so in this case the files were manually created and checked in". He also wrote that the file "is meant to stay local".
Apiiro's view: the ignore rule is in the wrong place. A global excludes file belongs to one user account on one machine. It is not part of the repository, so the protection does not travel with the project. Apiiro names the cases where it does not apply:
- a folder that becomes a git repository after the file already exists
- a clone on another machine, where Claude Code never wrote the excludes line
- a file someone already committed, because ignore rules do not apply to tracked files
- a Docker image built with a copy command, which carries the file along
Apiiro also says that Claude sometimes commits the file itself. It adds that Claude's commit instructions do not list this file among the files to leave out.
Both views fit the same facts. A global excludes line protects untracked files on one machine. A git add -f, an old commit, or a teammate's fresh clone is outside that protection. That is why the fix below puts the rule in the repository's own .gitignore, which every clone carries.
Practice the audit on a scratch repo
Run the audit once where nothing is at stake. This scratch repository commits a fake secret, the same way a manual check-in would:
mkdir -p ~/scratch-leak/.claude && cd ~/scratch-leak
git init
cat > .claude/settings.local.json <<'EOF'
{
"permissions": {
"allow": [
"Bash(mysql -u app -pFAKEpass123 shop)"
]
}
}
EOF
git add -f .claude/settings.local.json
git commit -m "add claude settings"git add -f stands in for the manual step Cherny described. Without -f, git refuses to stage a path that one of your ignore rules covers. With -f, git stages the file anyway. From then on the file is tracked, so no ignore rule applies to it.
Audit your repo for a leaked settings.local.json
Run these commands from the top of the repository you want to check. Each step answers one question.
1. Is the file tracked now?
git ls-files '*settings.local.json'Every path it prints is tracked. The quoted pattern also finds copies in subdirectories. No output means nothing is tracked today. History can still hold the file.
2. Was it ever committed?
git fetch --all
git log --all --oneline -- '*settings.local.json'--all covers every branch and tag you have locally, which is why the fetch comes first. Each line is a commit that added, changed or deleted the file. In the scratch repo you see the one "add claude settings" commit.
3. Does its history hold a password?
git log --all -p -- '*settings.local.json' '*.claude/settings.json' | grep -n -i -E 'passw|secret|token|key| -p[^ ]|://[^ ]*:[^ ]*@'This searches every added or removed line in both settings files. It looks for password and secret words, token and key names, -p followed directly by text (the MySQL client form), and URLs that carry a user and a password. In the scratch repo it prints the + line that holds -pFAKEpass123. Expect some harmless matches. Read each one anyway, because your list of credentials to rotate comes from this output, not from the current file.
4. Which ignore rule covers the file?
git config --global --get core.excludesFile
grep -n 'settings.local' ~/.config/git/ignore
git check-ignore -v .claude/settings.local.json; echo "exit=$?"Claude Code writes its line to core.excludesFile when your global git config sets that option to an absolute or ~ path. Otherwise it uses $XDG_CONFIG_HOME/git/ignore, or ~/.config/git/ignore when XDG_CONFIG_HOME is unset. If the first command prints a path, run the grep against that file instead.
For an untracked file that a rule ignores, git check-ignore -v prints one line in the form <source>:<linenum>:<pattern><TAB><pathname> and exits with status 0. The source is an absolute path when the rule comes from your global excludes file.
5. What check-ignore says about a tracked file
Run step 4 in the scratch repo, where the file is tracked. The git manual is clear about the result: "By default, tracked files are not shown at all since they are not subject to exclude rules". So you get no output and exit=1, which the manual defines as "None of the provided paths are ignored". This result is easy to misread. The rule can be sitting in your global excludes file. It does nothing here, because git never applies ignore rules to a file it already tracks.
To see the rule git would use if the file were not tracked, tell it to skip the index:
git check-ignore -v --no-index .claude/settings.local.json; echo "exit=$?"The manual describes --no-index as the way to debug why a path became tracked even though a rule should have ignored it. If you have never let Claude Code write the file on this machine, there is no global rule yet, and this command prints nothing either. A rule shown here, for a path that git ls-files also lists, is exactly what Apiiro found in public repositories.
Rotate every credential first, then untrack the file
Rotation is the real fix. Untracking only keeps the file out of future commits. Every earlier commit still holds it, and so does every clone or fork made while the repository was public. Rewriting history does not reach those copies either.
For each credential that step 3 found, change the password or revoke the token where it was issued. Then confirm that the old value no longer works. Rotate everything the file ever held, not only what it holds today.
After that, stop tracking the file and add the ignore rule to the repository itself:
git ls-files -z '*settings.local.json' | xargs -0 git rm --cached
echo '**/.claude/settings.local.json' >> .gitignore
git add .gitignore
git commit -m "Stop tracking Claude Code local settings"
git ls-files '*settings.local.json'
git check-ignore -v .claude/settings.local.jsongit rm --cached removes the file from the index and leaves your copy on disk. A pattern with a leading **/ matches the file in any directory. After the commit, git ls-files prints nothing. git check-ignore -v now prints the rule that covers the file and exits with status 0.
There are two follow-ups. First, delete any rule that holds a secret from your local settings.local.json, even if that copy was never committed. A secret in a plain file is one git add -f away from the next leak. Second, warn your teammates before they pull. When a pull brings in a commit that deletes a tracked file, git removes that file from their working tree. Anyone whose own approvals were in a tracked copy should back it up first.
Rewriting history is optional cleanup after rotation. It reduces what a future clone receives. It does nothing for clones and forks that already exist, and every collaborator has to clone again afterwards.
Stop the next leak: approve patterns, not literal commands
Keep secrets off the command line. If Claude never types the password, no rule can save it. Put the secret in a file outside the repository with mode 600. Give the project a small script that loads it, and approve the script instead of the database client.
mkdir -p ~/.config/myapp
install -m 600 /dev/null ~/.config/myapp/db.envWrite the rule yourself, with a wildcard. Use /permissions inside Claude Code, or edit the settings file. The current permissions docs define the syntax. A * in a Bash rule matches any text, including spaces. A rule with no * matches one exact command. Bash(ls *) matches ls and ls -la but not lsof, because the space before the * is part of the rule. Put the * after the subcommand. Claude Code warns at startup about an allow rule such as Bash(git * main), where the * stands in for the subcommand itself.
Deny reads of the secret file. Claude Code checks rules in this order: deny, then ask, then allow. An allow rule cannot carve an exception out of a deny rule. A .claudeignore file has no effect, so use Read deny rules:
{
"permissions": {
"allow": [
"Bash(./scripts/db-query.sh *)"
],
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(~/.config/myapp/**)"
]
}
}Put this in ~/.claude/settings.json so it covers only you. These rules have a limit. The docs say Read deny rules cover Claude's file tools. They also cover file commands that Claude Code recognizes in Bash, such as cat, and the targets of redirections. They do not cover a script that opens files itself. That is why your wrapper script still works, and it is also why some other script could read the file. For enforcement at the operating system level, the docs point to the sandbox. Keeping secrets out of an AI agent's reach covers that wider design, and running Claude Code safely on a VPS covers the server side.
Add a pre-commit check that reads command strings
Apiiro found that gitleaks and TruffleHog mostly let through passwords typed into commands, because those scanners look for known key formats. A password after a flag has no format to match. A blunt local check works better here. Save this as .git/hooks/pre-commit:
#!/bin/sh
# Refuse Claude Code's personal settings file, and flag likely secrets in .claude/ files.
if git diff --cached --name-only --diff-filter=ACM | grep -q 'settings\.local\.json$'; then
echo "pre-commit: refusing to commit a Claude Code settings.local.json" >&2
exit 1
fi
if git diff --cached -U0 --diff-filter=ACM -- '*.claude/*' | grep -i -E '^\+.*(passw|secret|token|api[_-]?key|bearer| -p[^ ]|://[^/@ ]+:[^/@ ]+@)'; then
echo "pre-commit: possible secret in a .claude/ file, see the lines above" >&2
exit 1
fi
exit 0Make it executable, then test it in the scratch repo:
chmod +x .git/hooks/pre-commit
git add -f .claude/settings.local.json
git commit -m "test the hook"
git log --oneline -1
git reset -q .claude/settings.local.jsonThe commit stops with the refusal message. git log still shows "Stop tracking Claude Code local settings" as the newest commit. The last line unstages the file again. --diff-filter=ACM limits the check to added, copied and modified files, so the commit that deletes the file from the index still passes.
This hook has limits. It lives in .git/hooks, which git does not copy when you clone, and git commit --no-verify skips it. Run the same check in CI (continuous integration) so it covers every clone. Apiiro also suggests adding the file to your organization's .gitignore templates. Cherny recommended keeping your git host's secret scanning on, which is still worth doing even though Apiiro found that these scanners miss passwords inside commands. Claude Code's own hooks work at an earlier point. Claude Code hooks run before a tool call, so a PreToolUse hook sees a command that contains a secret before it runs and before anyone approves it.
The shared settings.json and auto mode
The shared .claude/settings.json is meant to be committed. Apiiro found a real secret in 298 of 76,011 public copies, and says most of those secrets sit in allow rules written as full command lines. In the CLI (command-line interface), "don't ask again" writes only to your local file. The VS Code extension is different. Its approval card lets you pick the destination file, including the shared project file, which changes the rule for everyone. Audit step 3 already searches this file. Read every allow rule in it before you commit.
Trust works differently for the two files. Allow rules from the shared file apply only after each teammate trusts the folder, while deny and ask rules apply right away. A tracked settings.local.json is treated the same way. The permissions docs say that when the file is tracked in git, Claude Code "treats it as repository-supplied instead and holds its rules until you trust the folder."
Cherny's advice on HN was direct: "Permission rules are old hat. Everyone should be using auto mode instead." When you enter auto mode, Claude Code drops broad allow rules that grant arbitrary code execution, such as a blanket Bash(*) or a wildcarded interpreter like Bash(python*). It restores them when you leave. The docs add: "Narrow rules like Bash(npm test) stay in effect." A rule that holds a full command with a password in it is as narrow as a rule can be. So it stays in the file, and it stays in effect. Auto mode changes who reviews each action. It does not clean up what earlier sessions saved.
Auto mode may also save new rules. On HN, a reader suggested that auto mode has the same problem, because it still asks about sensitive commands and the default answer saves a rule. Cherny replied: "Yeah good catch. Thinking through what we can do to improve this (ideas welcome)." The permission modes page does not describe an auto mode prompt that saves a rule, and this guide did not test one. So check the file after sessions in any mode. How Claude Code auto mode decides what to approve covers the classifier in more detail.
Has Anthropic shipped a secret-scanning fix?
The changelog does not show one. Cherny wrote that "Historically Claude Code has not done secret scrubbing" and listed the options under discussion: "making sure auto mode catches this, building in basic secret scanning, a few other ideas." Apiiro's 8 November update says Anthropic plans to prototype these. On 9 November 2026, the five newest changelog entries, 2.1.314 to 2.1.319 (30 October to 6 November), do not mention secret scanning or secret scrubbing. The 2.1.317 fix for "don't ask again" on a mod's tool calls is about mods, not secrets.
Check the Claude Code changelog before you rely on a built-in check. A future fix will not help with secrets that are already out. A secret in a public commit stays exposed until you rotate it.
FAQ
Does Claude Code save my password when I choose "Yes, and don't ask again"?
It can. Choosing that option for a Bash command saves an allow rule to .claude/settings.local.json at the repository root. The docs show a prompt that saves a prefix such as npm test *. Apiiro reports cases where the exact command string was saved, password included. Open the file and read the rules to see what yours hold.
Is .claude/settings.local.json ignored by git automatically?
Only on one machine, and only while the file is untracked. The first time Claude Code writes the file in a git repository that does not already ignore it, it adds **/.claude/settings.local.json to your global git excludes file. That line does nothing on a teammate's machine. It also does nothing for a file that is already tracked. Add the same line to the repository's .gitignore so every clone carries it.
I removed the file from my repository. Am I safe now?
No. git rm --cached keeps the file out of future commits. Earlier commits, and every existing clone or fork, still hold it. Run git log --all -p -- '*settings.local.json' to find every credential the file ever held. Rotate each one, then confirm the old value no longer works.
Why does git check-ignore show nothing for settings.local.json?
The git manual says tracked files "are not shown at all since they are not subject to exclude rules". If you get no output and exit status 1 for a file that git ls-files lists, the file is tracked and your ignore rule does nothing for it. Run git check-ignore -v --no-index on the path to see the rule git would use, then untrack the file with git rm --cached.