Hardening Claude Code settings.jsonA complete config reference. A hardened baseline.
Claude Code ships with everything unlocked. One patch (Feb 2026) fixed env-var capture in settings.local.json, but the default permission model still lets the agent run arbitrary bash, reach the network, and touch files outside your project. The config to change that fits in 40 lines.
-
01
Default
settings.json wide open
all tools enabled, no deny rules, env visible
~/.claude/settings.json -
02
Hardened
deny + allowlist + isolation
bash scoped, network gated, env stripped
project .claude/settings.json -
03
Result
blast radius bounded
damage stays inside the project directory
no ambient credentials
TL;DR· the answer, in twenty seconds
What: Claude Code's default config leaves every tool enabled, inherits your full shell environment, and imposes no deny rules. An agent session with real credentials has the same reach as a terminal you left open and walked away from.
Fix: Add permissions.deny rules and scope bash to specific directories in ~/.claude/settings.json, then tighten further per-project in .claude/settings.json. The hardened baseline below takes 40 lines and survives a team review.
Lesson: Default-open configs fail at the worst time: long sessions, distracted developers, and agents chaining tool calls without confirmation. Explicit allowlisting is slower to set up and consistently safer than any deny list you try to maintain.
Claude Code's February 2026 patch removed the most obvious foot-gun: environment variables no longer land in settings.local.json by default. What the patch did not touch is the permission model. Out of the box, the agent can run any bash command, read any file your user account can read, fetch any URL, and install MCP servers you have never heard of.
Knostic's disclosure of the env-var capture (and their earlier Cursor .cursor/ find in late January) treated the two issues as separate bugs. They share a root cause: tools that observe your environment, store what they see, and operate with your ambient permissions. The settings.json hardening below addresses the second half of that root cause.
CVE-2025-59536, the command-injection bug Check Point published in early February 2026, is a reminder that the bash tool is a real shell that runs as you. Limiting what that shell can touch is not optional on repos with production access.
What to know in 60 seconds
- Claude Code reads
~/.claude/settings.json(global, all projects) and.claude/settings.json(per-project). Per-project overrides global. permissions.allowandpermissions.denycontrol which tools and shell patterns the agent can use. Deny rules take precedence over allow.permissions.defaultModecontrols what happens to unmatched tools:"ask"(default),"allow", or"deny".- MCP servers run as separate processes with their own tool surface. You can allowlist which servers the agent can call.
- Model pinning in
settings.jsonprevents a session from silently upgrading to a model with different behavior or pricing. - The per-project
.claude/settings.jsonshould be committed. The.claude/settings.local.jsonshould not.
Where the files live and what they do
Two files matter for hardening. The global file applies to every Claude Code session on the machine:
~/.claude/settings.json
The per-project file sits inside your repo and commits with the rest of your config:
<project-root>/.claude/settings.json
Before February 2026, Claude Code also wrote a third file, settings.local.json, that captured environment variables from each session. Anthropic patched that behavior. The file still exists for accepted permissions you have clicked through in the UI, but it no longer records env-var values. Verify this on your machine:
cat .claude/settings.local.json | grep -i "env\|secret\|key\|token\|password"
If anything prints, you have an old file from a pre-patch session. Rotate whatever it contains.
A hardened global baseline
The global settings file is the floor. Everything in it applies before any project-level config runs.
{
"model": "claude-opus-4-5",
"permissions": {
"defaultMode": "ask",
"allow": [
"Bash(git:*)",
"Bash(npm run *)",
"Bash(make *)",
"Bash(go build *)",
"Bash(go test *)",
"Read(*)",
"Edit(*)",
"Write(*)"
],
"deny": [
"Bash(curl *)",
"Bash(wget *)",
"Bash(nc *)",
"Bash(ssh *)",
"Bash(scp *)",
"Bash(rsync * root@*)",
"Bash(rm -rf /*)",
"Bash(sudo *)",
"Bash(su *)",
"Bash(chmod 777 *)",
"Bash(* | sh)",
"Bash(* | bash)",
"Bash(eval *)",
"WebFetch(*)",
"WebSearch(*)"
]
},
"env": {
"HOME": "${HOME}",
"PATH": "${PATH}",
"LANG": "${LANG}"
},
"mcpServers": {}
}
Three decisions worth explaining:
defaultMode: "ask" means any tool not matched by an explicit allow or deny rule triggers a confirmation prompt. You will see more prompts early. You will learn which patterns to add to the allowlist. That friction is the point: you discover what the agent actually calls before you whitelist it.
The deny list blocks outbound network tools at the bash level. Curl, wget, netcat, and ssh are the four highest-value exfil paths from a compromised agent session. Block them globally, then re-enable per-project if you have a specific need.
The env block strips every variable except PATH, HOME, and LANG. Claude Code inherits your full shell environment by default. Knostic's February finding showed exactly what happens when a debug file captures that environment. Stripping it at the config level means a tool that tries to log process.env finds nothing sensitive.
Tighten further at the project level
The global baseline is conservative. Individual projects need different permissions. A web scraper project needs WebFetch. A deployment script project needs ssh. Add those in the project-level file, not globally:
{
"permissions": {
"allow": [
"Bash(curl https://api.stripe.com/*)",
"Bash(npm publish)"
],
"deny": [
"Bash(npm publish --access public)"
]
},
"env": {
"NODE_ENV": "development"
}
}
The per-project file merges with the global. Deny rules from both files apply. An allow in the project file does not override a deny in the global file.
Commit this file. When a teammate opens the project in Claude Code, they inherit the same permission floor. Code review of .claude/settings.json is a real control: a PR that widens the allow list or removes a deny rule is visible in the diff.
MCP server allowlisting
MCP servers run as separate processes with their own tool surface. By default, Claude Code will connect to any MCP server defined in the project config. OX Security's early 2026 report counted roughly 7,000 MCP servers in the wild with no signature requirement. You cannot vet them all.
Add an explicit allowlist to the global settings:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/yourname/projects/myproject"],
"env": {}
}
}
}
The "env": {} in each server definition is load-bearing. Without it, the MCP server process inherits your full shell environment. An MCP server that logs its startup environment or writes a debug file has the same exposure profile as the pre-patch settings.local.json. Empty the env.
For servers you have not vetted, leave them out of the config entirely. If a project's README says to add a server you do not recognize, read the server's source before you run it. There is no sandbox.
Model pinning
Claude Code defaults to the latest available model in your subscription tier. A future model version may have different tool behaviors, different context limits, or different cost characteristics. Pin the model explicitly:
{
"model": "claude-opus-4-5"
}
This also matters for reproducibility. A session that used claude-opus-4-5 and one that used a different model may produce different tool call patterns for the same prompt. When you are debugging unexpected agent behavior, knowing the model is fixed removes one variable.
For CI environments, set ANTHROPIC_MODEL in the pipeline config rather than in a committed file. That way you can update the CI model without a PR.
The five most common foot-guns
These are the settings mistakes that appear most often in repos that have had incidents.
1. Committing settings.local.json. The local file holds per-session state. Before the February 2026 patch, it held environment variables. Even after the patch, it holds accepted permission choices that were meant to be ephemeral. Add it to .gitignore and .npmignore now.
echo ".claude/settings.local.json" >> .gitignore
2. Using defaultMode: "allow". This turns off all confirmation prompts for unmatched tools. Reasonable for a tightly-specified project where the allow list is complete. Actively dangerous during early development when the allow list is still forming. Most projects never move off this setting once they set it, because the prompts go away and nobody notices.
3. No env stripping. The default env block is empty, which means the agent inherits everything. Your AWS_ACCESS_KEY_ID, GITHUB_TOKEN, DATABASE_URL, all of it. The February incident happened because a state file captured this environment. An env block with explicit keys is the config-level fix. A broker like hasp goes one step further: secrets never enter the shell environment at all, so the agent process has nothing sensitive to inherit, even before the env block applies.
4. Global WebFetch(*) allow. This lets the agent retrieve any URL during any session. An agent session with web access and a confused goal can exfiltrate data by encoding it in a URL and fetching it. Allow WebFetch per-project, for specific domains, not globally.
5. MCP servers with inherited environment. Any mcpServers entry without an explicit "env": {} passes your full shell environment to the server process. Set "env": {} on every MCP server definition unless you have a specific variable the server needs, in which case set that variable explicitly.
What "deny" does not cover
Deny rules match against the tool call string Claude Code generates before it runs. A sufficiently clever prompt can produce tool calls that look different from your deny pattern while achieving the same effect. Bash(rm -rf /home) is not the same string as Bash(rm -r /home).
The deny list catches straightforward misuse. It does not replace filesystem permissions, network firewalls, or process isolation. On a shared machine or in a container with production mounts, deny rules are a convenience, not a security boundary.
The correct mental model: permissions.deny reduces blast radius during normal operation. It stops the agent from doing obviously destructive things by accident. For intentional misuse or prompt injection (CVE-2025-59536 is in this category), deny rules help but do not prevent.
A checklist you can paste into a PR
## Claude Code settings.json hardening
- [ ] ~/.claude/settings.json exists with explicit permissions block
- [ ] defaultMode is "ask", not "allow"
- [ ] curl, wget, nc, ssh, sudo in global deny list
- [ ] env block strips ambient credentials (only PATH, HOME, LANG)
- [ ] .claude/settings.json committed to repo
- [ ] .claude/settings.local.json in .gitignore and .npmignore
- [ ] model pinned explicitly
- [ ] mcpServers allowlist only servers you have reviewed
- [ ] every mcpServer definition has "env": {}
- [ ] WebFetch allowed per-project only, not globally
- [ ] git log --all -- '.claude/settings.local.json' returns nothing
Run this on every repo where Claude Code has run at least one session.
What this means for your stack
Settings hardening reduces ambient permissions. It does not solve the deeper problem: secrets still live in your shell environment, agents still request access to them, and the moment a state file or a log line captures that environment, you have an incident.
The durable fix is to move secrets out of the environment entirely. A local broker holds them in an encrypted vault, the agent requests access through it, and the broker injects values into specific child processes at exec time. The agent context window and any state files it writes hold references, not values. The next tool that ships a debug file finds nothing.
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 settings file is still worth locking down. Bounded blast radius matters. But a hardened settings.json and ambient credentials in your environment is still a bad combination. Fix both.
Sources· cited above, in one place
- Knostic Research on AI code editor secret leakage (Claude Code, Cursor)
- OX Security AppSec research, including MCP ecosystem analysis
- Check Point Research Claude Code command-injection disclosure (CVE-2025-59536)
- CVE-2025-59536 Claude Code command injection
- Anthropic Security advisories and Claude Code release notes
- 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.