You approve one useful command in Claude Code and move on. The command checked a database or called an API, so it carried a password or a token in plain text. Weeks later that same project goes to GitHub, and the approval you forgot is sitting in the repo for anyone to read.
That is the leak this tutorial stops. As we reported this morning in Claude Code Leak Exposes Passwords in 1,122 Repos, security firm Apiiro found 1,122 public copies of .claude/settings.local.json holding at least one secret. The file is not malware and no attacker put it there. Claude Code put it there when someone chose the option to not be asked again.
What You’ll Build or Learn
You will end with a repo that is safe to share and safe to keep using with Claude Code. The local settings file will stay on your machine and out of git. No saved permission rule in either settings file will hold a secret value. Your commands will refer to secrets by variable name, a local scan will check new work, and GitHub will block a push that carries a supported secret pattern.
You will also learn how to tell the two settings files apart, which is the skill that prevents a repeat. One file is yours alone. The other is built to travel.
| File | Who it is for | Should it be committed |
|---|---|---|
.claude/settings.local.json | You, in this one project | No. This is where standing Bash approvals are saved |
.claude/settings.json | Everyone in the project | Yes, after a careful review for secrets and private paths |
~/.claude/settings.json | You, in every project on this machine | No. It lives in your home folder, outside the repo |
The permissions documentation for Claude Code says a Bash approval saved with the option to not ask again goes to .claude/settings.local.json at the root of the git repo. The settings documentation says Claude Code adds that file to your global git excludes file the first time it writes it in a repo. That guard lives on one machine. It never goes into the repo ignore file, so it does not travel to a new clone, a teammate, or a runner.
Before You Begin
Read the order before you touch a file. Rotation comes before cleanup. A secret that sat in a public repo, or in git history, should be treated as public even after the file looks clean. Deleting the current copy does not remove older copies, forks, or clones.
Back up the two project settings files outside the repo before you edit them. Keep that backup out of git as well. Never paste a real secret into a bug report, a chat, a screenshot, or a shared log while you audit. The commands below are built to show file names, line numbers, and redacted output where the tool supports it.
Audit first, rotate second, clean third. Generated illustration by Dirty Static.
Step 1: Audit the Saved Rules in Both Settings Files
Action: open .claude/settings.local.json and .claude/settings.json in an editor and read every allow rule by eye. Then run /permissions inside Claude Code. The docs say that dialog lists all permission rules and the settings file each rule comes from, which helps you spot a rule you did not know you had saved.
grep -n "Bash(" .claude/settings.local.json
grep -n "Bash(" .claude/settings.json
Expected result: the searches list the saved Bash rules with line numbers. A rule with no wildcard covers one exact command. If that exact command holds a password, a token, a private key path with a value, or a database address with a password inside it, mark that value as exposed if the file was ever pushed. Do not copy the value anywhere new. Write down only the service name and the kind of secret, such as database password or API token, for the rotation list in Step 3.
Step 2: Check Whether the Local File Is Tracked and Ignored
Action: ask git two separate questions. Tracking and ignoring are not the same thing, and an ignore rule does nothing for a file that is already tracked.
git ls-files .claude/settings.local.json .claude/settings.json
git check-ignore -v .claude/settings.local.json
git status --short
Expected result: git ls-files prints the local file only if git tracks it. That printout is a problem, even if the second command finds an ignore rule. git check-ignore -v prints the ignore file and the pattern that matched when the file is ignored on this machine. No output means this machine would offer the file to a normal add. Note which ignore file the match came from. A match from your global excludes file protects this machine only.
Step 3: Rotate First If Any Secret Was Ever in Either File
Action: work through the rotation list from Step 1, plus any secret you find in history. For each value, go to the service that issued it, revoke or change the old value, issue a new one, store the new one in your secret store, update the place the app reads it from, and test the app once.
git log --all --oneline -- .claude/settings.local.json .claude/settings.json
Expected result: the log lists every commit that touched either settings file. If a listed commit reached a public remote, treat every secret in that version as exposed, even if the current file is clean. Apiiro counted hundreds of extra secrets in older versions alone. For a team repo, also search the GitHub organisation for the local file name, because a teammate clone may have pushed a copy you never saw. Rotation is slow and annoying. It is also the only step that makes an exposed value useless to whoever copied it.
Step 4: Untrack the Local File and Ignore It in the Repo Itself
Action: remove the local file from the git index only, so the copy on disk stays in place. Then add its pattern to the repo ignore file, so the rule travels with the project. Use the pattern form the Claude Code docs use for the global excludes entry.
git rm --cached .claude/settings.local.json
echo '**/.claude/settings.local.json' >> .gitignore
git add .gitignore
git status --short
ls -l .claude/settings.local.json
Expected result: status shows the local file as deleted from the index and the ignore file as changed, while the last command proves the local file still exists on disk for Claude Code to use. Commit the ignore change and the index removal together, with a message that says what you did and why. From now on a fresh clone has the ignore rule even on a machine where Claude Code has never run.
Step 5: Rewrite Risky Rules as Patterns and Move Secrets Into Variables
Action: in the /permissions dialog, delete every allow rule that holds a secret value, in both files. Then rebuild the useful approvals in a safe form. The permissions docs show the shape. A rule with a wildcard covers a family of commands. A rule with no wildcard covers one exact command, secret included.
| Saved rule shape | What lands in the file | Verdict |
|---|---|---|
| One exact command with a token value | The token itself, in plain text | Delete it and rotate the token |
A pattern such as Bash(npm run *) | The pattern and no value | Safe when the family is narrow |
| A command that names a variable | The variable name and no value | Best form for work that needs a secret |
Load the value into an environment variable through your secret manager or your password manager, outside any file you commit. Then let Claude Code run the command in its variable form:
curl -H "Authorization: Bearer $EXAMPLE_API_TOKEN" https://api.example.com/check
Expected result: if you approve that command with the option to not ask again, the text saved in the local file names the variable. It does not hold the value. Keep patterns narrow. A pattern that allows every curl call is a wider door than most projects need.
One more guard belongs in the shared file, after a team review. The settings docs use a read deny for environment files as their example, and deny rules in a project file apply right away. Add rules like these to .claude/settings.json so the read tool refuses secret files by default:
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.*)"
]
}
}
Expected result: Claude Code refuses to read those files with its read tool in this project. Treat this as one guard, not the whole fix. The fix is that no secret value sits in a command, a rule, or a tracked file in the first place.
Local scans and push protection are the gate. Rotation is what you do when the gate was open. Generated illustration by Dirty Static.
Step 6: Scan the Current Files and the Full History With Gitleaks
Action: install Gitleaks from its project repo. On macOS the repo lists the Homebrew route. Then scan the settings folder as plain files, and scan the repo with its full git history. The repo documents three scan modes. The git mode reads commit history. The dir mode reads files on disk.
brew install gitleaks
gitleaks dir --redact -v .claude/
gitleaks git --redact -v .
Expected result: each scan prints findings with a file, a line, and a rule name, with values redacted from the output. Go back to Step 3 for any real finding in history. A clean scan is not proof by itself. Apiiro ran Gitleaks and TruffleHog over the leaking files and found 215 files that neither tool flagged, most often because a plain password inside a normal command does not look like a known key format. Your eye audit in Step 1 is still part of the system.
Step 7: Add a Commit Hook So the Next Secret Stops Early
Action: add Gitleaks through the commit hook framework its repo documents. Install the framework first by following the install guide at pre-commit.com, then create .pre-commit-config.yaml at the repo root with the block from the Gitleaks repo:
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.24.2
hooks:
- id: gitleaks
pre-commit autoupdate
pre-commit install
pre-commit run --all-files
Expected result: the hook installs into this clone and a full run reports the current tree. Commit the config file so every teammate gets the same setup, and put the install command in the repo setup notes, because each clone still needs the install step run once. A local hook can be skipped by the person making the commit, so it is a guard for honest mistakes. The next step is the server side backstop.
Step 8: Turn On GitHub Push Protection
Action: open the repo on GitHub and go to Settings, then Advanced Security. GitHub docs say you must enable Secret Protection first if it is not already on. Under Secret Protection, to the right of Push Protection, choose Enable. The docs list repo owners, organisation owners, security managers, and users with the admin role as the people who can do this.
Expected result: GitHub now blocks a push that carries a supported secret pattern and raises an alert when a contributor bypasses the block. GitHub docs also describe a personal account form of push protection that guards your pushes to any public repo. Turn that on as well if your account offers it, because the leak in this story starts on a laptop, not on a server. Push protection covers known patterns. It does not replace the variable habit from Step 5 for plain passwords.
Verification
Work through this list in order. Every line should hold before you call the repo clean.
git ls-files .claude/settings.local.json
git check-ignore -v .claude/settings.local.json
git add .claude/settings.local.json
gitleaks git --redact .
The first command prints nothing, so the local file is not tracked. The second prints your repo ignore file as the source of the match, not only your global excludes file. A normal add attempt reports the file as ignored. Inside Claude Code, /permissions shows no rule holding a secret value, and a fresh approval for a safe command in variable form saves a rule that names the variable only. The history scan may still list old findings for rotated values. That is expected until your team makes a separate call on history. Rotation is what protects you in the meantime.
Troubleshooting
| Problem | Likely cause | Fix |
|---|---|---|
| The local file still shows as tracked after you add the ignore rule | Ignore rules do not untrack a file that git already tracks | Run Step 4 in full. The index removal is the untrack step, and the ignore rule keeps it out afterwards |
git check-ignore prints nothing | No ignore rule matches on this machine, or the file is already tracked | Add the pattern to the repo ignore file, commit it, and repeat Step 2 after Step 4 |
| A scanner reports no findings but you saw a password in a rule | The value is a plain password inside a normal command, which scanners often miss | Trust the eye audit. Rotate the value, delete the rule, and rebuild it in variable form |
| The risky rule comes back after a week | A new approval saved the exact command again | Check which command was approved, move its secret into a variable, and approve the variable form instead |
| The secret sits in the shared settings file | A rule or a value was saved or pasted into the team file, which is built to be committed | Rotate the value, remove it from the shared file, and review the file before every commit that touches it |
| The current file is clean but history still holds the secret | An older commit carried the file before you cleaned it | Keep the rotated value as the protection. Decide on history rewriting as a team, with notice to everyone who clones the repo |
| Push protection blocks a push you believe is safe | The pushed text matches a supported secret pattern | Read the block message, remove or replace the matching text, and use the bypass alert path only for a proven false alarm your security owner accepts |
Next Steps
Run this same audit on every repo where you use Claude Code, starting with the public ones. Then scan the other agent config folders you use with the same eye, because any tool that saves approvals or transcripts into a project folder can carry the same risk. Add the history scan to your build checks on a schedule, so new rules and new commits get checked without anyone remembering to run a command by hand.
The habit to keep is small. Approve patterns, not exact commands with values. Refer to secrets by variable name. Let the local file stay local, in the repo ignore file and not only in your global one. One approval click will still save you time. It just will not publish your passwords while it does it.
Facts checked November 8, 2026, against the Claude Code permissions and settings documentation, the Gitleaks project repo, and the GitHub push protection documentation. This tutorial follows the documented commands and the findings in the Apiiro research linked below. It does not claim the full sequence was run on a live account with real secrets.
Sources
- The leak, the counts, the scanner misses, and the rotate first order: Apiiro research, Coding agents leaking secrets on GitHub
- Where standing Bash approvals are saved, how wildcard rules work, and the
/permissionsdialog: Configure permissions, Claude Code documentation - The local file, the shared file, the global excludes guard, and the hand created file warning: Settings, Claude Code documentation
- Scan modes, install routes, and the commit hook config: Gitleaks project repo
- Who can enable push protection and where the setting lives: Enabling push protection for your repository, GitHub Docs
- Body illustrations: generated for Dirty Static. Hero image: Dirty Static.
END OF TRANSMISSION
Stay curious. Question everything.


