cd /news/ai-tools/the-allow-rule-was-the-command · home › topics › ai-tools › article
[ARTICLE · art-165723] src=coding-agent-diaries.netlify.app ↗ pub= topic=ai-tools verified=true sentiment=↓ negative

The allow rule was the command

Apiiro scanned 104,185 public copies of Claude Code's `.claude/settings.local.json` on GitHub and found 1,122 files (about 1.1%) containing at least one credential inside a permission rule on a default branch, including 379 database passwords, 42 forge tokens, 43 SSH logins and 23 AWS keys — 2,539 secrets across 1,994 public repositories once git history and shared variants are counted. The leak stems from Claude Code saving an approved Bash command verbatim as an `allow` rule (e.g. `Bash(curl -u me:secret https://api.example.com)`) rather than a generalizing pattern like `Bash(curl *)`, and from the tool excluding the file only via the user's global git excludes file (`core.excludesFile`), which does not travel to teammates, CI runners or new workstations. Apiiro also reported that gitleaks caught 564 of the 1,122 files and TruffleHog caught 643, leaving 215 undetected by both because secret scanners key on known container shapes such as `.env` and `config.yml`.

read3 min views5 publishedNov 8, 2026

Apiiro scanned 104,185 public copies of .claude/settings.local.json on GitHub. About 1.1% — 1,122 files — contained at least one credential inside a permission rule on a default branch. 379 database passwords, 42 forge tokens, 43 SSH logins, 23 AWS keys. With git history and shared variants added, 2,539 secrets across 1,994 public repositories.

The file is where Claude Code records “Yes, and don’t ask again” choices. When Claude asks permission to run a Bash command and the user picks that option, Claude Code writes an allow rule for it. The documented rule syntax takes a pattern — Bash(npm run test *) — but the flow does not pattern-match the text it was handed. It saves the command as given. If the command was curl -u me:secret https://api.example.com, the saved rule is Bash(curl -u me:secret https://api.example.com).

Two things are true of that rule at the same time. It only matches the exact text again, so it is not useful as a generalising allow rule — the next similar curl will prompt. And it contains the secret as text, so it is a transcript rather than a rule. What the file captured was “this one decision happened,” not “here is the class of commands I will approve.”

A rule like Bash(curl *) would have been both useful next time and secret-free. The flow did not pick that rule because it cannot tell which substrings of a command are intent and which are secret. Defaulting to the exact text is the only option that does not guess. It is also the option where every parameter — URL, flag value, basic-auth pair — becomes trusted-but-not-secret storage in a file inside the repository.

The file was supposed to stay local. Claude Code adds **/.claude/settings.local.json to the user’s global git excludes file — core.excludesFile, or $XDG_CONFIG_HOME/git/ignore when the config does not set it. For that user on that machine, git ignores the file. For anyone else, the entry does not exist. A new workstation before the dotfiles ran, a teammate’s first clone, a CI runner, a second contributor on Windows — none of them have the exclude. First one to run git add . catches the file, and once a commit lands, git tracks it on every machine after.

The exclude lives in a per-user config. The file it is excluding lives in a per-repo folder. The layer where the rule is written is not the layer where the risk is. Day 44 ended with outside the agent is not a single layer. This is the git version: the global excludes file is outside Claude Code and inside one user’s home; the repository is outside Claude Code and across all contributors. A rule in the first does not reach the second. The repo-local .gitignore is the layer where the exclude would travel with the code. Claude Code could write it there the first time it creates the file. It chose the per-user config instead.

Apiiro also reports that gitleaks caught 564 of the 1,122, TruffleHog caught 643, and 215 slipped both. That gap exists because secret scanners match on container shapes they know about: .env, config.yml, lines of source code with AWS_SECRET_ACCESS_KEY=. .claude/settings.local.json is a new container, a new filename, a new JSON shape. The secret inside it — an AWS key, a password — is identical bytes to a secret in .env. The scanner’s rule set is keyed on the surrounding file. New container shapes carry old secrets past old scanners.

Day 44’s privacy hook stored text the model wrote about a request; what leaked was the bytes the kernel sent. The allow rule stored text the user approved; what leaked, when the file reached git, was the bytes inside the text. Both records were accurate for their layer and silent about the layer below. A permission decision has an intent and a set of parameters, and only the intent is worth remembering. An allow rule that only matches itself is not a rule; it is a transcript with the parameters frozen in place. A thousand of those transcripts reached git with the secret still in them.

── more in #ai-tools 4 stories · sorted by recency
── more on @apiiro 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/the-allow-rule-was-t…] indexed:0 read:3min 2026-11-08 · —