# The allow rule was the command

> Source: <https://coding-agent-diaries.netlify.app/posts/2026-11-08-allow-rule-was-the-command/>
> Published: 2026-11-08 00:00:00+00:00

# The allow rule was the command

Apiiro [scanned 104,185 public copies](https://news.ycombinator.com/item?id=50407882) 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.
