· security · claude code · april 28, 2026 ·

Claude Code's settings.local.json has leaked 33 credentials to npm. Here's how to prevent it.

// figure The leak path: settings.local.json → git stage → npm publish → public exposure, with Tether and Vault gates marked
Schematic of the credential leak path with two intervention gates Four nodes connected left to right by arrows: settings.local.json, git stage, npm publish, and public registry. Two gates with X marks interrupt the flow — Vault between the workstation and the file, and Tether between the file and the git stage. // workstation // repo // publish // world .claude/ settings.local.json git add git commit npm publish tarball ↑ npm registry PUBLIC EXPOSURE VAULT TETHER cached creds staged in tarball scannable by attackers
// FILED Security · Secrets// SOURCE Septim Labs// PERMALINK /blog/claude-code-settings-credentials-leak-prevention.htmlcite this →
TL;DR
  • A scan of 46,500 npm packages published April 2026 found 428 packages containing Claude Code's .claude/settings.local.json file. Of those, 33 had live API credentials across 30 packages.
  • The leak path has four steps: Claude Code caches approved commands and credentials into settings.local.json → the file gets git-staged because nothing in .gitignore blocks it → npm publish bundles it into the tarball → the public registry serves it to anyone, including credential scanners.
  • The fix is two gates: keep secrets out of the project directory entirely (Septim Vault, $29) and block them from the git stage if they make it that far (Septim Tether, $19, $48 together).
  • Even without the products, this post includes a copy-paste .gitignore + .npmignore snippet that closes the obvious hole in 60 seconds.

On April 27, 2026, BD Tech Talks and Security Brief Asia published parallel reports of a scan that swept 46,500 npm packages looking for Claude Code's local settings file. The scan found 428 packages that included the file in their published tarball. Thirty-three of those files contained live API credentials — Anthropic keys, OpenAI keys, and a handful of cloud provider tokens — spread across 30 distinct packages. Every credential found this way is one a stranger can scan for, copy, and use before the package author notices.

This is a worst-case shape for a credentials leak. The keys are not in a private repository that got compromised; they are in public packages that any developer can npm install, including (especially) the people who would automate this kind of exfiltration. The window from publish to abuse is short.

The rest of this post walks the leak path one node at a time, explains why .gitignore alone does not fix it, and lays out the two-layer defense.

How the file ends up holding live credentials

Claude Code uses .claude/settings.local.json to remember per-project preferences that are too noisy to live in ~/.claude/settings.json. The most common entry is the allowlist of terminal commands: when you run a command Claude wants to execute and pick "always allow," that decision is saved here so it does not re-prompt next session.

The risk is in what else gets cached. From the BD Tech Talks coverage:

"Claude Code caches approved terminal commands in a hidden local file, and when developers select an 'allow always' option, credentials become permanently stored and may ship globally if the project is published to a public registry."

BD Tech Talks — April 27, 2026

Concretely, this happens when a developer runs a command with credentials inline — something like curl -H "Authorization: Bearer sk-ant-..." or aws s3 cp ... --profile prod — and Claude saves that command (with the credential as a literal string in the JSON) under the always-allow list. The saved entry might look like this:

{
  "permissions": {
    "allow": [
      "curl -H \"Authorization: Bearer sk-ant-api03-AbCdEf...\""
    ]
  }
}

The credential is now a string inside a file the developer never opens. The developer's mental model of where their secrets live no longer matches reality.

Why .gitignore does not save you

Claude Code's documentation does ship with a recommendation to add .claude/settings.local.json to .gitignore. That recommendation is real, and it works for new projects where the file does not yet exist when the developer first runs git init.

It does not work for the cases that produced the 33 leaked credentials. There are at least four ways a file ends up in the published tarball even when the project has a .gitignore:

  1. The file existed before .gitignore was updated. If the file was committed once before the .gitignore entry was added, git rm --cached is required to actually un-track it. Adding to .gitignore only stops new untracked files from being added; it does not retroactively un-track.
  2. npm uses a different ignore file by default. Without a .npmignore, npm falls back to .gitignore, which works in most cases. With a .npmignore, the gitignore is not consulted, and the developer has to remember to mirror the entry. The 428-package scan suggests this mismatch is widespread.
  3. The "files" field in package.json overrides everything. If package.json has a files array, anything in that array gets shipped — even if it appears in .gitignore and .npmignore. A "files": ["dist", ".claude"] entry that someone added once is the kind of mistake that silently ships every Claude config until the next person spots it.
  4. The build pipeline stages the file before publish. Some projects run a build step that copies the entire project directory into dist/, which then becomes the tarball. Once the file is inside dist/, the project root .gitignore is not in scope.

None of these is exotic. Each one is the kind of edge case that produces the kind of leak we are looking at.

The four-stage leak path

The schematic above breaks the path into four stages. Walking each one:

Stage 1: settings.local.json on the workstation

Claude Code writes the file. The developer never opens it. The file contains command strings; some of those strings contain credentials. This is normal and fine, until the file leaves the workstation.

Stage 2: git stage and commit

If .gitignore does not block the file (any of the four reasons above), then git add . stages it. git commit records it. The credential is now in the project's git history and will follow every subsequent clone.

Stage 3: npm publish

The tarball builder reads .npmignore first, falls back to .gitignore if .npmignore is missing, and respects package.json's files field if present. If the file made it through all three filters, it is included in the tarball. The author runs npm publish, the tarball uploads to the registry.

Stage 4: public exposure on the registry

The package is now on the public registry. Anyone can fetch the tarball — that is the entire point of a public registry. Automated credential scanners (which exist; this is a category of tool) extract the credentials and either flag them to a security team or exploit them, depending on who runs the scanner.

For an Anthropic API key specifically, the time-to-abuse window is about as short as it gets. A working key can be used immediately to run inference at the original owner's expense. The first sign for the owner is usually a billing alert, not a security alert.

// VERIFY THIS WEEK Run git log --all -- .claude/settings.local.json in any project you have ever published. If the command returns commits, the file is in your history; rotation is required even if the file is currently gitignored.

The two-gate defense

Removing the file from the tarball after the fact only helps for the next publish. The 33 credentials in the recent scan are already exposed; their owners need to rotate. For everyone else, the goal is to prevent any future credential from reaching stage 2 in the first place.

The structural answer is two gates — one in front of the file, one in front of the git stage.

Gate 1: keep secrets out of the project directory entirely (Septim Vault — $29)

Septim Vault — AES-256 dev-secret browser app

$29 ONCE · /vault

Vault is a local-first browser app that holds your dev secrets in encrypted storage and emits one-time shell exports on demand. Instead of pasting sk-ant-api03-... into a terminal command (where Claude can cache it), you copy a one-time exported variable that lives in your shell session and never lands in any file on disk.

The mental model: the project directory should never see a literal credential. If a credential string never lives in the directory, it cannot be cached, staged, committed, or shipped.

The wiring is small. After installing Vault and unlocking it, you generate a one-time export:

# In Vault: click "Export to shell" on the Anthropic key
# Vault copies a single-use line to your clipboard:
export ANTHROPIC_API_KEY=sk-ant-api03-AbCdEf...

# Paste into your terminal. The variable lives only in this shell.
# When you close the terminal, the variable is gone.

From here, any tool that reads $ANTHROPIC_API_KEY from the environment works normally. Crucially, you never type the literal credential into a Claude session, so Claude never has anything to cache.

Gate 2: block secrets from the git stage (Septim Tether — $19)

Septim Tether — 3 pre-commit hooks

$19 ONCE · /tether

Tether ships three hooks: a secret scanner that pattern-matches known credential formats (Anthropic, OpenAI, AWS, Google Cloud, generic high-entropy strings), a lint gate that runs ESLint or Prettier before the commit, and a branch-name enforcer. The secret scanner is the relevant one here: if any staged file contains a string matching the credential patterns, the commit is rejected and the developer sees the file path and line number.

This is the catch-net for the case where Vault is not yet in place, or where a developer typed the credential into a non-Claude tool. The pre-commit hook fires regardless of which tool wrote the file.

A typical Tether config that catches the settings.local.json case looks like this:

# .tether/config.yaml
hooks:
  - id: secret-scan
    targets:
      - "**/*.json"
      - "**/*.env"
      - ".claude/**"
    patterns:
      - anthropic_key
      - openai_key
      - aws_credentials
      - generic_high_entropy
    on_match: block
    message: "Credential pattern detected in {file}:{line}. Rotate and remove before committing."

The two gates work together. Vault keeps most secrets out of any file. Tether catches the ones that slipped through, regardless of which file they live in.

Close both gates this week

Septim Vault is $29 once. Septim Tether is $19 once. Together they cost less than the rotation labor on a single leaked Anthropic key, and once installed they run on every project you ship.

Get Septim Vault — $29 Get Septim Tether — $19 See both in /compare →

The 60-second free hardening

If you are not paying for either product today, the minimum hardening is still worth doing. The shape of the leaks in the scan suggests most of them would have been caught by these three lines.

1. .gitignore entry

# .gitignore
.claude/settings.local.json
.claude/*.local.json

2. .npmignore entry (always create this file even if it duplicates .gitignore)

# .npmignore
.claude/
.env
.env.*
*.local.json

3. Audit the files field in package.json

// package.json — check this list manually
{
  "files": [
    "dist",
    "README.md"
    // .claude should NOT appear here
  ]
}

If you have ever published a package and never explicitly checked these three, do it now. The check takes under a minute. The remediation cost — if you find a leak — is hours of rotation across every system that touched the credential.

What to do if you find a leaked credential

Assume the credential is in the hands of an attacker the moment you confirm it shipped. The remediation order:

  1. Rotate the credential immediately. Generate a new one, deploy it, and revoke the old one. For Anthropic keys, this is in the console under API Keys. For OpenAI, under API Keys in the org settings.
  2. Check usage logs for the leaked key. Look at the rate of requests since the publish date. Anomalous traffic patterns are a sign the key was actively abused.
  3. Unpublish the affected version (if recent). npm allows unpublish within 72 hours of publish for any version. After 72 hours, npm requires the package to be deprecated; the tarball stays on the registry forever, so this stops being an option fast.
  4. Rewrite git history if it is in your repo. Use git filter-repo or BFG to remove the file from history. Force-push (with team coordination). Then re-issue the rotated credentials.
  5. Add the gates. So the next leak does not happen.

The wider pattern

The Claude Code leak is one instance of a wider pattern: AI coding tools that cache state on the filesystem create a new class of files developers do not think about. .cursor/, .aider/, .claude/, .windsurf/ — each of these is a directory that did not exist three years ago, and each of them can hold strings that should not leave the workstation.

The defensive posture is the same regardless of tool: never type credentials into the tool's prompt, never paste credentials into a command the tool will save, and run a pre-commit secret scanner on everything that touches the repo. The tool will not enforce this; you have to.

Further reading