DIRTY STATIC / DISPATCH Tutorials08 NOV 2026 • 14 MIN READ
A giant caution yellow permission button on a concrete wall in a black and white server room, with an open settings file spilling paper keys onto the floor and hazard stripes underfoot
TutorialsINTELLIGENCE FOR THE MACHINE AGE

Stop Claude Code Leaking Secrets to GitHub

A single approval in Claude Code can save a password inside a project file that later reaches GitHub. This tutorial audits the saved rules, rotates anything exposed, untracks the local file in the repo itself, rewrites risky rules to use variables, and adds scanner and push protection layers that catch the next mistake.

Start reading ↓

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.

FileWho it is forShould it be committed
.claude/settings.local.jsonYou, in this one projectNo. This is where standing Bash approvals are saved
.claude/settings.jsonEveryone in the projectYes, after a careful review for secrets and private paths
~/.claude/settings.jsonYou, in every project on this machineNo. 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.

Inspection light over settings folders spread on a concrete table, one folder marked with a caution yellow stripe 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.

Linux · macOS · Terminal

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.

Linux · macOS · Terminal

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.

Linux · macOS · Terminal

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.

Linux · macOS · Terminal

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 shapeWhat lands in the fileVerdict
One exact command with a token valueThe token itself, in plain textDelete it and rotate the token
A pattern such as Bash(npm run *)The pattern and no valueSafe when the family is narrow
A command that names a variableThe variable name and no valueBest 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:

Linux · macOS · Terminal

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.

A steel gate and a caution yellow scanner beam checking a truck before it enters a concrete vault 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.

Linux · macOS · Terminal

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
Linux · macOS · Terminal

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.

Linux · macOS · Terminal

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

ProblemLikely causeFix
The local file still shows as tracked after you add the ignore ruleIgnore rules do not untrack a file that git already tracksRun Step 4 in full. The index removal is the untrack step, and the ignore rule keeps it out afterwards
git check-ignore prints nothingNo ignore rule matches on this machine, or the file is already trackedAdd 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 ruleThe value is a plain password inside a normal command, which scanners often missTrust the eye audit. Rotate the value, delete the rule, and rebuild it in variable form
The risky rule comes back after a weekA new approval saved the exact command againCheck which command was approved, move its secret into a variable, and approve the variable form instead
The secret sits in the shared settings fileA rule or a value was saved or pasted into the team file, which is built to be committedRotate 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 secretAn older commit carried the file before you cleaned itKeep 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 safeThe pushed text matches a supported secret patternRead 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

■

END OF TRANSMISSION

Stay curious. Question everything.

CONTINUE THE DESCENT

MORE FROM DIRTY STATIC ↘