Claude Code leaked secrets to npmWhat happened. What to do.
A coding agent's debug file ended up in 1-in-13 npm packages. The fix is one line of .npmignore. The lesson is what happens every time a tool quietly records your environment and you forget.
-
01
Source
settings.local.json
env vars captured
1 file · .claude/ -
02
Channel
npm publish
tarball ships .claude/
default include -
03
Exposure
registry + scrapers
scraped within minutes
1-in-13 packages
TL;DR· the answer, in twenty seconds
What happened: Claude Code stored a per-project settings.local.json that recorded environment variables, including secrets, from every session. When developers ran npm publish, that file shipped to the registry. Knostic and other researchers found the file in roughly 1 in 13 npm packages they scanned in February 2026.
The minimum fix: add .claude/ and settings.local.json to your project's .npmignore (and .gitignore) today. Run git log -p -S settings.local.json to check if you ever committed it. If you did, rotate the secrets, not just the file.
The lesson: any tool that "remembers context" between sessions is a candidate for the same failure mode. Stop trusting the tool to keep your environment quiet. Hand secrets to processes at execution time, then take them away.
The February 18, 2026 Claude Code disclosure was not a prompt injection. It was a state file shipping environment variables into public npm packages.
Claude Code writes settings.local.json into your project's .claude/ folder on the first agent session. The file holds accepted permissions, trusted tools, and your model choice. It also recorded any environment variable the agent's child process saw. Run a session with STRIPE_KEY exported in your shell, and the value landed in the JSON.
npm publish then put that JSON into the tarball. The .claude/ path is missing from npm's default ignore list and from the npm init .gitignore. Knostic scanned the registry in February and found the file in about 1 in 13 published packages. GitGuardian's 2026 State of Secrets Sprawl tracks the same pattern across coding agents: AI-assisted repos leak around 40% more often than the baseline.
If you published an npm package between November 2025 and February 2026 with Claude Code in the repo, assume you leaked.
The short version of what went wrong
Three failures stacked.
One. Claude Code wrote a state file containing live environment variables to a predictable on-disk path. No widely-used .gitignore template filtered .claude/ yet.
Two. Developers committed .claude/settings.local.json. Some treated it as project config. Some never noticed it. The filename reads like preferences.
Three. npm publish ships .claude/ unless you tell it not to. PyPI does the same with MANIFEST.in. Most Docker base images that COPY . will too.
No exploit. No zero-day. A tool observed your environment, persisted it to a predictable path, and your publishing pipeline did its job. Nobody broke anything, which makes the cleanup harder than a breach.
What to do right now, in order
If you ship code to a registry, work through this list top to bottom.
1. Stop the bleed locally
Open a terminal in any repo where you have used Claude Code:
echo ".claude/" >> .gitignore
echo "settings.local.json" >> .gitignore
echo ".claude/" >> .npmignore
echo "settings.local.json" >> .npmignore
Python projects: add recursive-exclude .claude * to MANIFEST.in. Verify pyproject.toml is not re-including it.
Docker images: add .claude/ and settings.local.json to .dockerignore. The COPY . pattern pulls in dotfiles unless you stop it.
Go binaries: go mod ignores the file. go:embed patterns will pick it up. Grep your repo for embed.FS and check what each directive matches.
This stops future publishes. It does not retract anything you already shipped.
2. Check whether you have leaked
Run this from your project root:
git log --all --oneline -- '.claude/settings.local.json' '*.claude/*'
Any output means git tracked the file at some point. Inspect each diff:
git log -p -- '.claude/settings.local.json'
Look for API keys, bearer tokens, and database URLs with embedded credentials. The file is short. Read it.
For each published version of an npm package, pull the actual tarball from the registry and grep it:
npm pack <your-package-name>@<version>
tar -tf <your-package-name>-<version>.tgz | grep -i claude
Repeat for every version published since Claude Code touched the repo.
3. Rotate, do not delete
Any value that looks like a secret was scraped within minutes of publication. That includes database URLs with credentials in the query string, OAuth tokens in JSON, and bearer headers stashed in test fixtures. Bots watch the npm release feed. Plan around a short window.
Rotate the credential. Deleting the file in a later commit does nothing. Git keeps the value in history. Even a history rewrite leaves the npm tarball intact, because the registry treats published versions as immutable.
For each leaked credential:
- Provider tokens (Stripe, OpenAI, Anthropic, GitHub, Vercel): revoke in the provider dashboard, issue a new key, update consumers.
- Database URLs with embedded passwords: rotate the database password, update connection strings.
- OAuth client secrets: regenerate in the OAuth provider settings.
- Long-lived JWTs: invalidate via JWKS rotation if the signing key is intact. Otherwise rotate the signing key.
Fast path: a secret manager rotates once and updates downstream consumers. Slow path: .env files scattered across fifteen subprojects. Block out the afternoon.
4. Tell the people who depend on you
Maintainers of public packages: file a GitHub Security Advisory on the repo. Set severity, name the leaked credential categories, do not publish the values. Downstream consumers need to rotate any copies of your token they cached or embedded.
The advisory is embarrassing. A blog post in three months naming your package is worse.
5. Add a publish-time guard
A prepublishOnly script in package.json will fail the publish if the file exists:
{
"scripts": {
"prepublishOnly": "test ! -e .claude/settings.local.json && test ! -e settings.local.json"
}
}
Python projects: mirror the check in your release script. GitHub Actions: add a step that greps for the file and exits non-zero before the publish step runs.
Run this in CI, not only in a pre-commit hook. Pre-commit ships with a --no-verify bypass. CI does not.
How to harden Claude Code itself
Anthropic patched the recording behavior in late February. The file shrank. Environment variables stay out unless you re-enable the capture. The patch covers new writes only. Files already on disk in repos you have not opened in a month still hold whatever they captured.
Steps for repos that still hold real secrets:
- Get secrets out of your shell environment. Stop
export STRIPE_KEY=...in~/.zshrc. Hold project-scoped secrets in a source the agent reads only on demand, for one child process at a time. - Strip the env Claude Code inherits. Wrap the launch:
env -i PATH=$PATH HOME=$HOME claude code .... Crude. Layer specific keys back in for the commands that need them. - Audit
.claude/on every push. A pre-push hook that greps staged content for.claude/settings.local.jsonruns in under a second. - Set
CLAUDE_PROJECT_CONFIG_PATHoutside the repo. The file still lands on disk, in~/.cache/claude/<project>/instead of./.claude/. It will not enter the package tarball because it does not live in the package tree.
CI helps the picture. Runners use ephemeral filesystems, so the file dies with the job. Exposure shifts to log retention: a debug step that runs cat .claude/settings.local.json puts the contents into your action log for 90 days. Skip the cat.
What gets missed in the conversation about this incident
The format was not the problem. A long-lived, predictably-named state file containing environment data is the problem. Write the same file as .claude/state.bin in protobuf and bots still scrape it. JSON made post-disclosure inspection easier. It did not cause the leak.
The vendor is not the story. Cursor, Codex, Aider, and most MCP servers keep per-project state files of their own. Different filenames and contents, identical shape: long-lived, repo-local, environment-aware. Knostic disclosed the equivalent issue in Cursor's .cursor/ directory three weeks earlier. Check Point published a separate command-injection bug in Claude Code project files at the start of February (CVE-2025-59536). The category is "coding agents persist state into your repo." Anthropic is one example.
Credential theft pays better than prompt injection. The highest-EV attacks on AI tooling are old plays in new wrapping: credential harvesting, supply-chain insertion, log exfiltration. Prompt injection gets the conference talks. Credential harvesting funds the operations.
One commenter on the disclosure thread put the framing more bluntly: "prompt injection gets attention because it's novel, but stolen credentials are a classic attack with way higher impact."
A better runtime model
Stop framing this as a Claude Code bug. The runtime model fails:
- secrets live as long-lived environment variables
- coding agents can read your full environment
- agents persist some shape of that environment to disk
Two of those three produces incidents on a long enough timeline. All three guarantees them.
A safer model swaps each leg:
- Secrets sit in one local, encrypted store. Out of your shell. Out of
.envfiles spread across the project. - Agents request access at the call site. They receive a reference or a metadata view. The value lives elsewhere.
- Values appear inside a specific child process at exec time and vanish when it exits. The agent context window stays clean. Nothing reaches the log stream or a state file.
- An append-only audit log records every grant. "Did the agent touch the prod token between 2 and 4pm Tuesday?" returns in three seconds.
That is runtime brokering. Today's default treats the agent as a process that reads ambient state and stores what it likes. That default produced the February incident. Without changing it, expect the next version of this story in six months with a different tool name.
A short checklist you can paste into a PR
## Coding-agent leak audit
- [ ] `.claude/`, `.cursor/`, `.aider/`, `.codex/` in `.gitignore`
- [ ] Same paths in `.npmignore` / `MANIFEST.in` / `.dockerignore`
- [ ] `git log --all -- '.claude/*'` clean
- [ ] `npm pack` of last 5 versions checked for agent state files
- [ ] `prepublishOnly` / CI step that fails the build if state files exist
- [ ] Pre-push hook that scans for `settings.local.json` content
- [ ] Confirmed no `cat .claude/*` calls in CI logs (90-day retention)
- [ ] Rotated any credentials present in leaked files
- [ ] Filed GitHub Security Advisory if package was public
- [ ] Notified downstream consumers
Paste it into your repo's SECURITY.md or a PR template. Re-run it whenever a new agent tool lands in the project.
What this means for your stack
The minimum is the .gitignore and .npmignore change above. Past that, the question is whether you want a runtime model where this category of incident has no surface to land on.
The shape of that model: a local broker holds secrets in an encrypted vault, agents request access through it, the broker injects values into specific child processes at exec time, and an HMAC-chained audit log records each grant. The agent reads a reference, not the value. State files the agent writes hold references too. The next vendor that ships a debug file capturing environment data finds nothing worth taking.
hasp is one working implementation. curl -fsSL https://gethasp.com/install.sh | sh, hasp setup, connect a project, hand the next session a reference instead of a key. Source-available (FCL-1.0), local-first, macOS and Linux, no account.
The lesson holds whether you use hasp or not. Long-lived ambient environment variables are the unsafe surface. Stop assuming the agent reads only what it needs from your shell.
February's leak was a config file. The next leak will be a different file with a different name. The set of paths a vendor can write to in your repo is finite. The set of ways those paths land in a published artifact is much larger. The durable fix is to keep the sensitive values out of the directories the tool writes to.
Sources· cited above, in one place
- Knostic Research on AI code editor secret leakage (Claude Code, Cursor)
- GitGuardian State of Secrets Sprawl report
- Check Point Research Claude Code command-injection disclosure (CVE-2025-59536)
- CVE-2025-59536 Claude Code command injection
- npm files / .npmignore documentation
- Functional Source License FCL-1.0 text
Stop handing the agent your real keys.
hasp keeps secrets in one local encrypted vault, brokers them into the child process at exec, and never lets the agent read the value.
- Local, encrypted vault — no account, no cloud, no telemetry by default.
- Brokered run — agent gets a reference, the child process gets the value.
- Pre-commit + pre-push hooks catch managed values before they ship.
- Append-only HMAC audit log answers "did the agent touch the prod token?" in seconds.
macOS & Linux. Source-available (FCL-1.0, converts to Apache 2.0). No account.