Updated 2026-11-10. A leaked key is a credential sitting somewhere you published. Rotation is the step that ends the exposure, and deleting the line that leaked it does not, because the old copy stays readable. The order below is what a business with no security team should do in the first hour.
# Minute one: is a key sitting in something git is carrying? git grep -nE '(Bearer[[:space:]]+[A-Za-z0-9._-]{20,}|sk-[A-Za-z0-9_-]{20,}|gh[pousr]_[A-Za-z0-9]{30,})' $(git rev-list --all) 2>/dev/null | head -20
A key is a password that software uses instead of a person. Whoever holds it can act as you at whichever service issued it: send email from your domain, read the customer records stored there, spend against your account, or move files in and out.
That is why the response is ordered the way it is. The question is not "how do we hide it again", it is "who else has it, and what can they do with it until it stops working". Only the vendor can answer the second half, and only issuing a new key answers the first.
One thing makes this different from a leaked password: a repository keeps history. Every commit that ever contained the key is stored, and anyone with a clone has all of them. Removing the file in a new commit is housekeeping, not remediation.
Two of the five are often skipped. Step 4, because the usage pages are buried and nobody wants what they might show. Step 5, because the customer conversation is unpleasant, and it is the step that has an actual legal shape if the key reached personal data rather than, say, a font library.
This is the sequence, with the commands an engineer will run. I have run all of them in a throwaway repository and in this site's own repository, and the outputs below are copied from those runs.
First, find out whether the key is in something git is carrying:
# Is the file tracked at all? git ls-files --error-unmatch .claude/settings.local.json 2>/dev/null && echo TRACKED || echo "not tracked" # Across every commit, not just the current tree: git grep -nE '(Bearer[[:space:]]+[A-Za-z0-9._-]{20,}|sk-[A-Za-z0-9_-]{20,})' $(git rev-list --all) 2>/dev/null
Then stop tracking it, in a way that does not break anyone's clone:
git rm --cached .claude/settings.local.json printf '.claude/settings.local.json\n.claude/*.local.json\n' >> .gitignore git add .gitignore git commit -m "Stop tracking agent-local settings and ignore them" git push
Then prove to yourself that step 3 did not finish the job:
git show 9b5a0ed:.claude/settings.local.json | grep -c Authorization
Run against this repository's copy, that command returns 1. One hit, in a commit from October, still readable by anyone who clones. Which is why the order in the table puts rotation second and deletion third: the deletion is the part that feels like progress and it is the part that changes the least.
1
Rotation itself happens in a browser, not a terminal. Sign in to the vendor, create a new key, put it in your environment or secret store, deploy, confirm the software still works, then delete the old key. Deleting before deploying breaks your own tools for ten minutes and teaches everyone to avoid this work.
You do not need a malicious insider. The four routes we see, in order of how often they explain what happened:
A person approved a command in an AI coding tool once, and the tool wrote the command into a local file alongside the key value. Anthropic's documentation describes the behaviour: choosing "Yes, and don't ask again" saves the approval as an allow rule in .claude/settings.local.json, and the file is kept out of git when Claude Code creates it, with the instruction to add it to .gitignore yourself if you created it by hand [1]. If that file was already tracked, or if the tool ran somewhere that is not a git repository, the approval lands in the shared .claude/settings.json [1], which is the file teams are told to commit.
.claude/settings.local.json
.gitignore
.claude/settings.json
Someone pasted a key into a config file to test something and committed before removing it. This is the ordinary case, and it is the one a scanner catches.
Someone put a key in a client-side file or a deployed static asset, where it is public by definition. A browser needs no permission to read a file your server hands out.
A key was shared in a document, a ticket or a chat to unblock someone, and the document outlived the reason.
It is worth being blunt about the safety net, because most owners assume one exists. GitHub's own documentation sets out the scope: secret scanning "runs automatically for free" on public repositories; organization-owned private and internal repositories need GitHub Secret Protection enabled; user-owned repositories need GitHub Enterprise Cloud with Enterprise Managed Users [3]. It also reports rather than blocks by default, with push protection disabled unless enabled, and write-access users can bypass it with a reason.
So the practical reading for a small business: if your code is private, in a repository owned by a person or a small account, nothing scanned it and nothing emailed you. Our own repository is exactly that shape. I found the key in it by running the check in this article, not by receiving an alert.
Anthropic's position is also current rather than historical. A Claude Code engineer wrote on 2026-11-07 that the team is "going to prototype a few of these to understand the tradeoffs better before we ship something" [4], and the dated changelog has not moved past that behaviour since. Treat this as a live characteristic of how agent tools work, not as a bug that was patched last week.
Not every key deserves the same first hour. Rank by what the key can reach, not by what it is called:
If you cannot tell which row a key belongs to, that is itself the finding. It means nobody has written down what the tool is allowed to do, which is the same gap that lets an agent do work nobody asked for, as we described in the post on unrequested agent work. The two habits that close it are deciding what an agent may approve and finding the agents nobody approved.
The first hour after a leak is expensive. This hour, once a quarter, is what makes it rare. Run it the same way every time, and keep the output, because a written record of "we checked on this date" is what you show a client or an insurer later.
#!/usr/bin/env bash # quarterly-agent-check.sh: one pass over the things agents are allowed to read and do. set -uo pipefail echo "== 1. keys sitting in tracked files, across all commits ==" git grep -nE '(Bearer[[:space:]]+[A-Za-z0-9._-]{20,}|sk-[A-Za-z0-9_-]{20,})' $(git rev-list --all) 2>/dev/null | head echo "== 2. agent config files, and whether git ignores them ==" for f in .claude/settings.local.json .claude/settings.json .mcp.json; do [ -f "$f" ] || continue if git ls-files --error-unmatch -- "$f" >/dev/null 2>&1; then echo "TRACKED: $f"; else echo "ok: $f ($(git check-ignore -v -- "$f" 2>/dev/null || echo untracked))"; fi done echo "== 3. what the agent is allowed to read ==" grep -n '"deny"' -A6 .claude/settings.json 2>/dev/null || echo "no shared deny rules; add them"
On a clean repository it prints nothing under the first heading, ok lines under the second, and your deny list under the third. On an unclean one it prints the commits, which is the whole point: you want the bad news in a terminal on a Tuesday, not in a support ticket on a Friday.
ok
The audit script that ships with the developer version of this article, scripts/audit-agent-permissions.mjs, does the same pass with masking and an exit code you can put in CI. Real output from a repository that had the problem:
scripts/audit-agent-permissions.mjs
FAIL .claude/settings.local.json [TRACKED] bearer token on line 5: sk-l...00 (44 chars) bearer token on line 8: sk-l...00 (44 chars) fix: git rm --cached this file, add it to .gitignore, rotate the key checked 1 candidate file(s), 1 tracked file(s) carrying a secret
And the two errors you will meet while cleaning up, exactly as git prints them:
$ git check-ignore -v x fatal: not a git repository (or any of the parent directories): .git
$ git rm --cached .claude/settings.local.json fatal: pathspec '.claude/settings.local.json' did not match any files
The second one normally means the file was never tracked, which is the answer you want. It can also mean you are in a subdirectory and the path is relative to the repository root.
Everything above is administration. The part that needs a person is the sentence you write for the case where a key reached customer data. Who tells the client, in what words, and when. Small teams tend to improvise this into a phone call that starts with an apology and ends without a record, and the record is the part that protects you.
Write that sentence once, now, while nothing is on fire, and keep it next to the rotation steps. Ours is short: what was exposed, what it could reach, what we did, when it stopped working, and what we changed so it cannot happen again.
Someone already deleted the line. Are we safe? No. The value is still in the repository's history and still valid until the vendor issues a new one, so rotation is the step that closes it.
Do we need to rewrite history to remove the old commit? It is optional and the tradeoff is real: rewriting a shared branch needs a force push, which breaks every other clone, and it does not help if the repository was public or already cloned. Rotate first, then decide. We do not force push, here or for clients.
Our repository is private, so does this apply? Private limits who can read it and does not remove the key from it. It also removes the automatic alert, since GitHub only scans public repositories for free.
How long does rotation take? Ten to thirty minutes for most services, and it is the single step that makes the leaked value worthless. Everything else in this article is either diagnostics or prevention.
What if we do not know which keys exist? Start with what the software runs on, not with a wish for a complete list: environment variables, the secret store, the files an agent reads, and the tools it can reach. A written intake is a reasonable way to have someone else do that inventory once.
Will our agents keep doing this? The pattern is current, the vendors are aware, and nothing shipped yet [4]. The durable fix is on your side: deny rules for the files that hold secrets, which Anthropic documents as the right home for exactly this, since Claude Code then "excludes matching files from file discovery and search results, denies reads of them, and blocks the Edit and Write tools on the matching paths" [2].
Rotate first, because the key is worthless the moment the vendor issues a new one and worth everything until then. Then untrack and ignore the file, so the next commit does not repeat it. Then read the usage log, and write down what you found and who you told. That is an hour, and it is the same hour whether the key belongs to a payment provider or a weather API.
If you would rather have this done across your repositories and your team's habits before it is urgent, send a written intake describing where the agents run and which keys they can reach. We run this as an embedded function for small teams, and we publish what we learn, including the accident we found in our own repository.
[1] Anthropic, Claude Code settings, fetched 2026-11-09. Source of the approval-to-allow-rule behaviour, the rule that the file is kept out of git when Claude Code creates it, the instruction to add it to .gitignore yourself, and the locations where the approval lands somewhere shared.
[2] Anthropic, Claude Code settings reference, fetched 2026-11-09. Source of the documented purpose of permissions.deny and of the rule that a tracked local settings file is treated as repository settings.
permissions.deny
[3] GitHub Docs, about secret scanning, fetched 2026-11-09. Quoted for which repository types are scanned automatically, and for the reporting-versus-blocking distinction.
[4] Hacker News comment by the Claude Code engineer bcherny, 2026-11-07T21:21:55Z. Quoted for the statement that prototypes come before a shipped fix.
bcherny
Maxpertise is an AI-native engineering company. We embed native AI engineers inside your team, live in about 10 days. Please enable JavaScript to view the site, or email contact@maxpertise.net.