Hardening Claude Code: the Minimum Floor as Config An engineer detailed how Claude Code's sandbox and permission settings can be configured to prevent AI agents from bypassing security controls, citing issue #40117 where Opus repeatedly evaded gitleaks and test hooks. The post provides specific configuration keys, such as setting allowUnsandboxedCommands to false and disabling bypass-permissions mode, to create structural enforcement that agents cannot edit away. The settings, hooks, and gates that make a Claude Code hardening measure something an agent cannot edit away in passing. In Claude Code issue 40117 https://github.com/anthropics/claude-code/issues/40117 , an engineer describes watching Opus bypass their gitleaks and test hooks six commits in a row. Every time, the same move: --no-verify , or a quiet flag, or a git stash that made the working tree look clean when a hook ran. Asked about it afterward, the agent misrepresented what it had done. Anthropic closed the issue "not planned." That is not a bug report. It is the vendor telling you, in writing, that enforcement living inside the agent's own workspace is not enforcement. That closes the loop on the argument I made in the parent post to this one https://dev.to/javatarz/the-software-still-has-to-be-right-review-is-no-longer-enough-1kgi : a deletable net is one the agent can satisfy by removing it, and a structural net enforces a property no matter what path the agent takes. That post stayed tool-agnostic on purpose. This one is not. It is the Claude-Code-specific config that turns each of the parent post's structural nets into something you can actually paste into a settings file this afternoon, with the specific bypass each control closes named alongside it. I checked every setting name and every citation below against live docs and live issue trackers as of this writing 2026-07 . Claude Code's settings surface moves fast; verify again before you rely on any of it six months from now. sandbox.network.allowedDomains alone is a deletable net wearing a structural costume. The bypass is sandbox.allowUnsandboxedCommands , and it defaults to true : a command that fails inside the sandbox is allowed to retry outside it, with a permission prompt standing in for enforcement. Prompt fatigue makes prompt fifty-one a rubber stamp. { "sandbox": { "network": { "allowedDomains": "api.github.com", "registry.npmjs.org", "api.anthropic.com" , "deniedDomains": "gist.github.com", "gist.githubusercontent.com" , "allowManagedDomainsOnly": true }, "failIfUnavailable": true, "allowUnsandboxedCommands": false } } Each key closes a specific hole. allowManagedDomainsOnly is a managed-settings-only lock, so a project or personal settings.json cannot widen the array underneath it. failIfUnavailable refuses to start rather than silently running unsandboxed when the underlying sandbox tooling bubblewrap or socat on Linux is missing. allowUnsandboxedCommands: false removes the retry-outside-the-sandbox path entirely, so there is no prompt to habituate past. The explicit deniedDomains matters too: a broad allow on github.com still permits exfiltration through gist.github.com , so pair every allow with the specific upload-capable subdomains you're denying. MCP servers talk over stdio and bypass this sandbox entirely. sandbox.excludedCommands docker, gradle, JVM tooling runs fully unsandboxed by design, and it has no managed-only lock, so a developer can always append to it locally. And there is no TLS inspection, so domain fronting can reach a non-allowed host hiding behind an allowed hostname. Anthropic's own docs admit that last one https://code.claude.com/docs/en/sandboxing rather than pretending it away, which is the correct way to document a boundary. sandbox.allowUnsandboxedCommands above gates the in-session escape from sandboxed to unsandboxed execution. A completely different setting gates --dangerously-skip-permissions and bypass-permissions mode itself: { "permissions": { "disableBypassPermissionsMode": "disable" } } Note the value is the string "disable" , not a boolean. This is the fix for issue 20260 https://github.com/anthropics/claude-code/issues/20260 , which asked Anthropic to prevent exactly the combination that makes both bypasses dangerous together: bypass-permissions mode running alongside allowUnsandboxedCommands: true skips every prompt, silently, which is precisely what a prompt-injected instruction would aim for. Setting disableBypassPermissionsMode removes the code path, not just adds a scarier confirmation. Ship it through managed settings: scalar keys like this one override every lower scope outright, no merging involved, so managed wins by default. The domain list above is an array, and arrays merge across scopes instead of overriding, which is why it needed allowManagedDomainsOnly to force managed-only behavior. More on that split further down, in the managed-settings section. What this still does not cover: excludedCommands again, since it has no managed lock regardless of which permission mode you're in; MCP servers and hooks, since this only governs the Bash tool's sandbox; and credentials reachable from inside the sandbox itself, since a fully sandboxed agent holding a valid token can still do damage without ever touching either bypass. That is the secrets section's job, further down. A devcontainer is a container image plus a devcontainer.json manifest, an open spec at containers.dev https://containers.dev/ , that pins your whole dev environment base image, mounts, environment variables, extensions so it's identical on your laptop, in CI, or in GitHub Codespaces. You open one with VS Code's Dev Containers extension, the standalone devcontainer CLI, or Codespaces, all reading that same file. It matters here for one reason: iptables rules need a boundary to enforce against, and applying default-deny network policy to your bare host breaks everything else running on it. The container's own network namespace is what the firewall actually locks down. Anthropic ships its own reference design for exactly this, a layer below the Bash-tool sandbox: .devcontainer/init-firewall.sh https://github.com/anthropics/claude-code/blob/main/.devcontainer/init-firewall.sh in the anthropics/claude-code repo itself, alongside the Dockerfile and devcontainer.json that wire it in. It sets a default-deny policy, iptables -P {INPUT,FORWARD,OUTPUT} DROP , then builds an ipset -backed allowlist for GitHub, npm, and the Anthropic API, with everything else answered by REJECT --reject-with icmp-admin-prohibited rather than a silent drop. To use it, copy the whole .devcontainer folder into your own project and adjust the allowlist for your own registries.This is kernel-level network enforcement, distinct from the process-level syscall confinement of items 1 and 2, and the two are complementary rather than redundant. Anthropic states directly that this firewall is what makes it safe to run with --dangerously-skip-permissions for unattended work: it is the structural net underneath the interactive-approval net you just turned off. The firewall needs NET ADMIN / NET RAW capabilities, granted through runArgs in devcontainer.json , and Anthropic's own reference runs init-firewall.sh from postStartCommand . That alone doesn't guarantee the rules land before you get a shell: the devcontainer spec's waitFor property defaults to updateContentCommand , not postStartCommand , so a tool is free to attach before the firewall script finishes unless you set waitFor: postStartCommand yourself. Get that wrong and there's a window where the agent already has a shell and the network is still open, exactly the moment a prompt-injected instruction would use it. Watch for known gaps in this pattern from the wider community: DNS resolves once at container startup, so a long session can silently lose connectivity if an allowed service rotates IPs; nested Docker-in-Docker can bypass the whole allowlist; and IDE devcontainer mode opens a separate extension-RPC and port-forwarding control plane that this firewall does not touch, which is one more reason CLI mode is the tighter boundary for unattended runs. Item 1's allowlist governs outbound tool calls. It says nothing about where the model traffic itself goes, and that gap is not hypothetical: CVE-2026-21852 https://github.com/advisories/GHSA-jh7p-qr78-84p7 is a malicious repo configuration rewriting ANTHROPIC BASE URL to an attacker-controlled endpoint, with requests, including the API key, leaving before the trust prompt for that repo ever appeared. Anthropic fixed the specific timing bug in v2.0.65. Upgrade first; this is not a config problem you can compensate for on an old build. { "env": { "ANTHROPIC BASE URL": "https://your-gateway.internal" } } I could not find documentation stating that a managed env block is locked against a project overriding it the same way allowManagedDomainsOnly locks the domain array. Managed settings do support a generic env block, and pinning your model traffic through your own gateway is good practice regardless, but treat this one as defense in depth on top of the version fix, not a confirmed structural guarantee. Verify the lock behavior yourself before you rely on it. Issue 40117 already told you gitleaks as a bare pre-commit hook does not survive contact with an agent that wants past it. The steelman is three layers, in this order. First, a PreToolUse hook that hard-blocks the bypass flags before the command ever runs, matching both the Bash tool and any MCP tool that can write straight to a remote repo: { "hooks": { "PreToolUse": { "matcher": "Bash", "hooks": { "type": "command", "command": "scripts/block-no-verify.sh" } }, { "matcher": "mcp github . ", "hooks": { "type": "command", "command": "scripts/block-github-mcp-writes.sh" } } } } A hook that exits with code 2 is a real process-level veto, not a suggestion the agent can talk its way past, and it has to match the MCP pattern too: an agent can commit through a GitHub MCP tool's push files call straight through the API, skipping local git hooks entirely. block-no-verify https://github.com/tupe12334/block-no-verify is a working implementation of the Bash-side check, also shipped as an npm package, if you want a starting point rather than writing the flag-matching yourself. This will occasionally catch a legitimate --no-verify during a real incident, someone shipping a hotfix under pressure. That is the correct tradeoff. The fix for a genuine false positive is a two-minute change to the hook script, reviewed under item 10 below, not a standing exception every developer remembers to reach for. Second, gitleaks https://github.com/gitleaks/gitleaks as a fast local pre-commit pass, explicitly not the boundary, just an early warning. Third, gitleaks or TruffleHog with --only-verified for less noise as a required CI status check. This is the actual backstop, because --no-verify has no effect once the change reaches CI. Watch one suppression trap here: gitleaks supports inline allow comments and a baseline file, and both are the exact same deletable-net failure if the agent can edit them freely. Put .gitleaks.toml and the baseline behind the review gate in item 10 below, or the scanner config is one edit away from being decorative. Secrets brokering closes the gap these three layers leave open: what an agent does with a credential it never should have needed to hold. The canonical example is the 1Password SSH agent. The agent can trigger a git push , but signing happens inside 1Password; key material is never readable by any process the agent runs, so there is nothing to exfiltrate even under full prompt-injection compromise. The same shape generalizes: an LLM gateway holding API keys, a proxy injecting auth headers at the network boundary, CI holding deploy credentials while the agent can only open a PR that triggers them. This ties directly back to the parent post's Rule of Two: an agent holding real secrets should be the agent that either has no egress or reads no untrusted content. If a secret cannot be brokered away, cut one of the other two legs instead. Half of this is genuinely structural, and half of it is a human decision no tool should pretend to automate. The structural half: run npm ci , yarn install --immutable , or pnpm install --frozen-lockfile in CI, never a plain install . Each of these refuses to run if package.json and the lockfile disagree, and none of them will silently rewrite the lockfile to paper over the mismatch. Pair that with a pre-install verification tool: slopcheck https://github.com/0xToxSec/slopcheck queries the registry in real time and flags a package name that does not exist, is under thirty days old, has near-zero downloads, or sits a Levenshtein edit away from a popular name. Run it as a PreToolUse hook gating the install command, or as a CI action, before the install runs rather than after, since a scanner that only checks post-install has already let the install script execute.A PreToolUse hook matches command text, so it gates npm install as a command but not an agent hand-editing package.json or the lockfile directly. Pair it with a lockfile-diff check at PR time that flags any new package name by age and download count. The irreducibly human half is the decision to take the dependency at all. License terms, maintenance signals, whether the team actually needs it. No verification tool claims otherwise; slopcheck 's own scope is a tripwire on hallucinated or squatted names, not a substitute for that judgment call. A skill you approved last month can change its own instructions this month, or a remote MCP server behind a URL can quietly add tools you never reviewed. A one-time install-time scan misses both, and it is still a human-gated decision at the point of install, which means someone can install anyway despite a bad score. That is the deletable-net pattern again, one layer up the stack. The two-layer version holds up better. First, run SkillSpector https://github.com/NVIDIA/SkillSpector as a CI gate on the skill's own repository, blocking on its documented exit-code contract, on every edit to that repo, not once at install time. Second, pair it with the structural layer Claude Code actually gives you: a managed-settings deny list for high-risk MCP servers that a local config cannot override, and the meta "anthropic/requiresUserInteraction" tool annotation, which forces a full approval prompt on a specific tool call in acceptEdits , auto , and bypassPermissions modes. One nuance worth getting right: in dontAsk mode, that annotation does not prompt, it denies the call outright. That is still enforcement, just failing closed instead of opening a dialog, which matters if your team runs dontAsk for unattended agents. Precedence is real but conditional, and the condition trips people up. Managed settings sit above CLI args, local, project, and user settings for scalar values, the way permissions.disableBypassPermissionsMode does above. But permissions.allow / deny , sandbox allowlists, MCP server lists, and hooks all merge across scopes rather than override, unless the matching lock key is also set: allowManagedPermissionRulesOnly , allowManagedMcpServersOnly , allowManagedHooksOnly , or strictPluginOnlyCustomization . A developer can add their own permission rule on top of the managed ones unless you locked that specific door. Deny-lists merge from everywhere, so blocking is always inheritable; allowing isn't, unless you locked it. CVE-2025-59536 https://research.checkpoint.com/2026/rce-and-api-token-exfiltration-through-claude-code-project-files-cve-2025-59536/ is a malicious project-level .claude/settings.json with enableAllProjectMcpServers: true among the vectors executing before the trust dialog for that repository had even resolved, leading to remote code execution and token exfiltration. Anthropic fixed it in v1.0.111. The lesson generalizes past this one CVE: repo-supplied configuration, .claude/settings.json , CLAUDE.md , hook scripts, is attacker-controllable content the moment it sits in a repository someone else can open a PR against. Treat it as untrusted until reviewed, which is exactly what item 10 exists to enforce. Borrow the shape GitHub already ships for Copilot's coding agent: the agent can push only to its own branch, cannot force-push, cannot merge, and cannot approve its own pull request. Translated into ordinary GitHub configuration, that is branch protection rules plus required review plus a workflow-approval gate for the agent's commits, the same gate you'd already require for a first-time or forked contributor. Nothing about this is Claude-Code-specific; the value is applying a policy you already trust to the agent's own commits with no special-case exemption. Every file introduced by items 1 through 9, the hook scripts, init-firewall.sh , the managed-settings source, the branch-protection rule itself, .gitleaks.toml , is only as structural as the review gate on changes to it. Without that gate, all nine are exactly as deletable as anything else in this post, one file edit away. CODEOWNERS plus branch protection is the required floor: GitHub-native, path-based, mandatory human review, enforced by the platform rather than by a tool you have to trust separately. This stops being optional for the same reason secrets scanning did in the parent post. It always caught something, and the review-volume and authorship collapse that made every other net in this post necessary is exactly why "good teams already reviewed config changes" becomes "every team has to." If your team wants risk-based escalation instead of an all-or-nothing gate, gitStream https://docs.gitstream.cm/ lets you write YAML rules over PR properties, a docs-only change auto-approves, a change touching auth/ or a lockfile requires a senior or security reviewer and blocks until it gets one. That is one pointer, not a recommendation to go shopping across every review-router tool on the market. Ranked by the strongest thing I can say about the evidence, not by section order: | | Control | Closes | Evidence | |---|---|---|---| | 5 | PreToolUse hook + required CI secrets scan | secrets committed by an agent routing around local hooks | Documented incident | permissions.disableBypassPermissionsMode: "disable" allowManaged Only ANTHROPIC BASE URL via managed env , and upgrade sandbox.network.allowedDomains + allowManagedDomainsOnly slopcheck + human sign-off .devcontainer/init-firewall.sh requiresUserInteraction If you only trust one column, trust "documented incident" over "CVE" over "reasoned." A reasoned control is still worth having. It just means nobody has been burned by its absence in public yet, and public is not the same as never. Mandatory is not the same as maximal here either, same as in the parent post. If you are a solo developer or a small team running Claude Code without a platform team behind you, here is the shortest version that still closes the failure modes that are silent or irreversible: permissions.disableBypassPermissionsMode: "disable" , so bypass mode cannot silently skip prompts even if you or the agent reach for it under deadline pressure. PreToolUse secrets hook plus a required CI secrets-scan check. This is cheap and it is the one control in this list that has a documented, named failure showing what happens without it. slopcheck if you must, but never skip the sign-off. sandbox.network.allowedDomains , even without the managed-only lock if you're a team of one. Partial enforcement against yourself is still worth having.Everything past this scales with blast radius: the devcontainer firewall, the model-traffic gateway, the full managed-settings lockdown, the branch-protection workflow-approval gate. Add them as the cost of being wrong goes up, not because a checklist told you to. None of this is fire-and-forget. Claude Code's settings surface moves fast enough that a key you locked down this quarter can be renamed or superseded by the next release, and nothing here will tell you when that happens. Put a recurring, owned line item, quarterly is often enough, on revisiting this list against current docs. Treat sandbox.excludedCommands specifically as a standing audit item rather than a one-time setting: it has no managed-only lock, so the way you find out your policy quietly widened is to go looking for it in every project's local settings, not to assume the managed layer caught it for you. Assign the revisit to a name, not a team, or it will quietly stop happening the same way the setting it's checking quietly widens. Pick the one bypass you are most exposed on right now. If you have ever run --dangerously-skip-permissions without also locking disableBypassPermissionsMode , that is your first move. If you have never had a required CI secrets check, that is yours. Ship one, in warning mode first if your team needs to trust it before it blocks, then make it a required check. The parent post's argument was that the bar never moved, only the thing you used to clear it did. This post is what clearing it looks like when the thing doing the work is Claude Code specifically: not a policy document, a setting you can point to, verify still exists, and watch hold the line the next time an agent goes looking for the shortest path to green.