← Back to all posts
Published on · by Renaud Deraison

The allow rule kept the password

On November 6, Apiiro reported that Claude Code's "Yes, and don't ask again" button saves the approved command, secrets included, in .claude/settings.local.json, and that 1,122 public GitHub repositories had committed that file with live-looking passwords, tokens and keys in it. Two popular secret scanners missed 215 of them. In Bromure Agentic Coding, every secret the agent can type is a stand-in, so a saved rule holds a value that works nowhere, and every push waits for you on the Mac.

You click "Yes, and don't ask again" to stop a prompt from coming back. Claude Code stores the whole command so it can recognize it next time, password and all, in a file inside your project.

On November 6, Apiiro published research on coding agents leaking secrets through "don't ask again". The Hacker News covered it the next morning.

The mechanism is ordinary. When Claude Code asks to run a shell command and you pick Yes, and don't ask again, it writes an allow rule into .claude/settings.local.json in the project. Sometimes the rule is a pattern like npm test *. Sometimes it is the exact command, and when the exact command had a password after PGPASSWORD=, sshpass -p or mysql -p, the password is now in a JSON file next to your code.

Apiiro searched public GitHub for that file and found:

  • 104,185 copies in 103,617 repositories;
  • 1,122 copies holding at least one secret, 1,379 distinct secrets in all;
  • 379 database passwords, 43 SSH logins for public servers, 42 GitHub, GitLab and npm tokens, and 23 AWS keys;
  • secrets belonging to 63 organizations, including a database vendor's admin-level cloud key and the owner password of a medical-records database.

Every one of those files was still on its default branch when Apiiro checked. In half the repositories it had been there for at least 217 days. Older commits held 734 more secrets across 509 repositories, and 211 forks of 74 repositories carried copies. Counting the shared .claude/settings.json, history and saved agent chat logs, Apiiro counted 2,539 secrets in 1,994 repositories.

One click, four steps to a public secretApproval promptPGPASSWORD=s3cr3t psql-h db.example.com …"Yes, and don't ask again"Allow rule saved.claude/settings.local.jsonthe full command, in plain textCommitted539 / 1,105traced repos where a commitco-authored by Claude touched itPublic on GitHub1,122copies with secrets; halfthere 217 days or moreDeleting the file doesn't delete the secret734 more secrets in older commits across 509 repositories; 211 forks of 74 repositories carry the same values
How a password gets from an approval prompt to a public repository. The allow rule stores the exact command. The agent commits the project folder, the file goes up with it, and every fork and every old commit keeps a copy after the default branch is cleaned.

Why it got past everything

Claude Code does try to keep the file out of git. The first time it writes settings.local.json, it adds the file to your global git excludes. That rule lives on one computer. A teammate's laptop, a CI runner or a fresh clone doesn't have it, and it does nothing for a file that's already committed. Claude Code never adds the file to the project's own .gitignore. Out of 16 coding agents Apiiro compared, Claude Code is the only one confirmed to save the full approved command in a project file by default.

The secret scanners did no better. Apiiro ran gitleaks and TruffleHog over every copy. Gitleaks flagged 564 of the 1,122 files, TruffleHog 643, and 215 got past both. Most of those 215 were passwords typed into commands. Scanners look for known key formats like AKIA… or ghp_…, and sshpass -p hunter2 doesn't match any of them.

Users reported the problem more than a year ago. An August 2025 issue, anthropics/claude-code#5544, said Claude Code stored approved commands with API keys word for word in this file. A bot closed it as "not planned" after 60 days without activity. Four related reports met the same bot, the latest one the day before The Hacker News article.

The secret was in the command because it was in the agent's hands

Every one of those 1,379 secrets was a value the agent could type. A developer pasted it into the chat, or left it in a .env file, or the agent read it from ~/.aws/credentials or ~/.pgpass and inlined it into the command it wanted to run. The allow rule wrote down what the agent already had.

Apiiro's advice for the future points the same way: keep secrets out of commands, and reference a variable from a secrets manager so the saved rule only holds $DB_URL. That helps if every developer and every agent follows it every time. An agent trying to get a failing command to work will expand the variable. Bromure takes the other route and keeps the real value out of the agent's environment altogether.

The same click, in Bromure Agentic Coding

Bromure Agentic Coding runs Claude Code, Codex, Grok, Kimi and omp in Ubuntu VMs on your Mac. The agent gets credentials through what we call the wire boundary: inside the VM, every secret is a stand-in, and the real value lives encrypted on the Mac, with its master key in the macOS Keychain. A proxy on the Mac swaps the stand-in for the real value on the wire, and only on a request to the host that credential belongs to.

So when the agent asks to run a command with a token in it and you pick Yes, and don't ask again, Claude Code saves the command exactly as before. The token in it is the stand-in.

Every secret the agent can type is a stand-in

Stand-ins keep the shape tools expect, so nothing in the project changes:

What Apiiro found leakedWhat the agent holds in a Bromure machine
GitHub and GitLab tokensghp_ + 36 characters, glpat- + 20, in GH_TOKEN, GITLAB_TOKEN and ~/.git-credentials
npm tokensa brm_… value in NPM_TOKEN, from an Other API key entry scoped to registry.npmjs.org
AWS keysthe real access key ID with a fake secret key; the Mac re-signs each request with the real one
Database passwordsa brm-db-… value in the variable you name, for ClickHouse, the Mongo Data API and Elasticsearch
SSH loginsno password at all: signatures from keys that stay on the Mac

Take a command an agent might run against a ClickHouse endpoint:

curl -u default:brm-db-7Qk2…Xw9 \
  'https://analytics.example.com:8443/?query=SELECT+count()+FROM+events'

Approve it forever and the allow rule holds brm-db-7Qk2…Xw9. The proxy swaps the stand-in on requests to analytics.example.com, in the Basic-auth header and in the request body, since database secrets often ride inside SQL and JSON. Anywhere else, the string is useless.

For AWS, the secret never reaches the VM. The agent's ~/.aws/config points at a helper that hands out the real access key ID with a 40-character fake secret. The AWS CLI, boto3 and terraform sign with the fake, and the Mac strips that signature and re-signs with the real secret before the request leaves. The real secret key never exists in the VM, so no command the agent writes can contain it.

SSH covers the 43 server logins in Apiiro's count. A Bromure machine's SSH_AUTH_SOCK is a bridge to the Mac: the VM can ask for signatures, and the private key bytes never cross over. Keys behind a YubiKey, Secretive or 1Password can be offered one by one, with the touch prompt on your Mac. The agent never needs sshpass -p, so there's no password for an allow rule to save.

Machine VMApproved commandGH_TOKEN=ghp_Xk3…a7Q gh api …"Yes, and don't ask again"settings.local.jsonholds the stand-in, which worksonly through this Mac's proxygit pushthe commit includes the fileYour MacProxy swaps on the wirestand-in → real token, only onrequests to github.comReal values, Keychain-sealedtokens, AWS secrets, SSH keys:never in the VM in any formPrompt before writethe push waits for your answerThe internetapi.github.comgets the real token, thecommand works as usualSomeone copies the filefrom a repo, a fork or an oldcommit and tries the token:401 Bad credentials
The same click in a Bromure machine. The allow rule still stores the command, but the token in it is a stand-in. The push that would publish it waits for a dialog on the Mac, and a stand-in someone copies from GitHub fails everywhere, because only the proxy on your Mac can turn it into the real value.

A stand-in on GitHub opens nothing

Say the file does reach a public repository, a fork and a dozen old commits. Whoever scrapes it gets ghp_ and 36 characters that GitHub has never issued. Sent from their laptop, it fails authentication. Only the proxy on your Mac knows which real token it maps to, and that proxy only serves your machine's VM.

If a stand-in ever heads for the wrong host from inside the machine, the proxy treats it as an exfiltration attempt. It blocks the request with HTTP 451, pauses the VM, and opens an alert titled This environment may have been compromised that names the credential, the host it belongs to and the host it was sent to. That makes each stand-in a tripwire as well as a placeholder.

Rotation happens in one place

Apiiro's first instruction is to rotate every password, token and key the file ever held, before cleaning anything up. With real secrets scattered across .env files, shell history and agent config, that is a hunt. In Bromure, the real values sit in one place: the machine's Credentials pane on the Mac. Paste the new token there and the stand-in in the VM stays the same, so the allow rules, scripts and configs inside the machine keep working.

An Other API key can also be a 1Password reference such as op://Engineering/npm/token. Bromure resolves it on the Mac and re-resolves it every 2 minutes, so a key rotated in 1Password takes effect within a couple of minutes, with no restart and nothing in the VM to edit.

Moving secrets into that pane takes one step. Import env file… reads an existing .env or ~/.bashrc, shows the variables with masked values, and turns them into credentials. The project folder no longer needs the .env file the agent was reading passwords from.

The push waits for you

In 539 of the repositories Apiiro traced, a commit crediting Claude as co-author changed the file. Agents commit and push as part of finishing a task.

On new Bromure machines, every forge service starts in Prompt before write in the Guardrails pane. Reads pass. A git push over HTTPS pauses for a dialog on the Mac showing the operation, and nothing leaves until you answer. The policy runs in the proxy, so nothing inside the VM can switch it off.

A reviewer that reads the line

The 215 secrets no scanner caught were passwords inside commands. A model reading PGPASSWORD=… psql -h db.example.com in a committed JSON file knows what it is looking at.

A Bromure code review watches a GitHub repository with an agent on one of your machines, on Every new commit, Every new pull request or a weekly schedule, and Bromure deduplicates its findings across scans. For Apiiro's "search your organization, including history" step, Run Baseline Now starts a one-off review of the whole repository. GitHub is polled from the Mac, and the review agent works with the same stand-ins as any other.

The transcript holds the stand-in too

Apiiro's count included saved agent chat logs. Claude Code's own session files in the VM record what the agent saw, and in a Bromure machine that is the stand-in. Bromure's traces on the Mac keep token swaps as short previews like sk-a…f9q3, and the real secret bytes never enter a trace record. When a real-looking secret does show up in traffic, because someone pasted one into the VM by hand, the trace flags it as a potential leak, and bromure-cli trace leaks lists every one. Turn on Only allow credentials Bromure injects in Guardrails, and a request carrying a credential Bromure didn't put there is blocked and raises the compromise alert.

Stand-ins in every command

Tokens, database secrets and AWS keys in the VM are stand-ins. An allow rule saves a value that works only through your Mac.

No password for SSH

The VM asks the Mac for signatures. The private key never crosses over, and sshpass -p is never needed.

Pushes wait on the Mac

Prompt before write is the default for new machines. A push pauses for your answer, enforced in the proxy.

Rotate once

Real values live in one pane, or in 1Password. Change them there and every stand-in keeps working.

What to do this week

From Apiiro's list, for every team using coding agents:

  • Search your GitHub organization for .claude/settings.local.json and .claude/settings.json, history included.
  • Rotate every secret those files ever held, then clean up.
  • Run git rm --cached .claude/settings.local.json and add it to the project's .gitignore and your organization's templates.
  • Treat .claude/, .cursor/, .gemini/ and .specstory/ like .env files in pre-commit and CI scanning, and make sure the scanner catches passwords inside commands.

Then take the real secrets out of the agent's reach. In a Bromure machine, the next "don't ask again" saves a stand-in, whether or not the developer remembered Apiiro's rules that day.

For the wider pattern of agents copying secrets into history and logs, see The transcript kept the token. For how the swap works in detail, read the Credentials chapter of the manual.