Post

Claude Code Has Two Security Layers — And They Do Very Different Things

Claude Code's permission rules and its OS-level sandbox are two completely separate mechanisms. Understanding both is the key to a setup that's both usable and secure.

Claude Code Has Two Security Layers — And They Do Very Different Things

I spent some time recently tuning my Claude Code permission settings to avoid needing --dangerously-skip-permissions, and I learned something that changed how I think about Claude Code security entirely: there are two completely separate security layers, and they operate independently of each other.

Getting this distinction wrong means you’re either more exposed than you think, or more locked down than you need to be.

The Two Layers

Layer 1: Permission Rules — this is Claude Code’s built-in prompting system. When Claude wants to run a bash command or edit a file, it checks your permission rules to decide whether to ask you first. This is what --dangerously-skip-permissions bypasses, and what your allow/deny rules in settings.json control.

Layer 2: The OS Sandbox — this is a completely separate mechanism powered by Apple’s Seatbelt on macOS (or bubblewrap on Linux). It enforces hard limits at the operating system level, regardless of what Claude Code decides to do.

These two layers don’t know about each other. You can have one without the other, or both at the same time.

What Each Layer Actually Controls

The permission rules layer decides: will Claude ask before doing this? It’s a gate on Claude’s behavior, not on what the OS allows. If you add Bash(git *) to your allow list, Claude will run git commands without prompting you. That’s all it does.

The sandbox layer decides: will the OS allow this at all? It enforces:

  • Filesystem writes — locked to your project directory and a few temp paths. A rogue script in a package can’t touch ~/.bashrc or drop files anywhere unexpected.
  • Network access — outbound connections are restricted to domains you’ve approved. A curl to an unlisted domain will fail at the OS level, regardless of whether your permission rules allow Bash(curl *).

Here’s the key insight: --dangerously-skip-permissions only affects Layer 1. If you have the sandbox enabled, it stays active even when you use that flag. Claude stops asking for permission, but the OS still enforces its limits. In practice, this means the sandbox is a meaningful safety net even in fully autonomous mode.

Configuring the Sandbox’s Network Restrictions

The sandbox’s network allow list lives in ~/.claude/settings.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
  "sandbox": {
    "enabled": true,
    "autoAllowBashIfSandboxed": true,
    "network": {
      "allowedDomains": [
        "github.com",
        "softwarethug.com"
      ],
      "allowLocalBinding": true
    }
  }
}

The allowedDomains list is what constrains curl, rsync, git push, and any other Bash command that makes a network connection. Think carefully about what you actually need. For a Jekyll blog, I only need two domains: github.com for git operations and my own server for deployment via rsync. The npm and RubyGems registries are irrelevant because packages are already installed.

allowLocalBinding: true is required if you run any local dev servers — without it, the sandbox blocks port binding and your server won’t start.

One Blind Spot: Reads Are Wide Open

The sandbox restricts writes aggressively, but reads are unrestricted by default. Claude can read any file on your filesystem — other projects, ~/.ssh/, /etc/, anywhere. It just can’t write outside your project directory.

If that bothers you, you can explicitly lock it down:

1
2
3
4
5
6
7
8
9
"sandbox": {
  "filesystem": {
    "denyRead": [
      "~/.ssh",
      "~/.aws",
      "~/.gnupg"
    ]
  }
}

For a personal dev machine where you trust yourself, this is probably fine. But it’s worth knowing the gap exists — especially if you’re working with external code or in a sensitive environment.

Another Blind Spot: WebFetch and WebSearch Bypass the Sandbox

Here’s one that surprised me. Claude’s built-in WebFetch and WebSearch tools don’t route through your machine’s network stack at all — they go through Anthropic’s infrastructure. That means allowedDomains doesn’t restrict them. Claude can still fetch any URL via those tools regardless of what you put in the sandbox network config.

This is actually the right behavior for usability — you want Claude to be able to research things — but it’s worth understanding that the sandbox’s network restriction is specifically about what Bash commands can reach, not what Claude itself can look up.

How Permission Rules and Sandbox Work Together

Once I understood the two-layer model, I restructured my settings to put the right things in the right places:

Permission rules (allow/deny) control which commands Claude runs without asking. They’re about reducing friction for your workflow. I moved universal dev tools to my global ~/.claude/settings.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
  "permissions": {
    "defaultMode": "acceptEdits",
    "allow": [
      "Bash(git *)",
      "Bash(docker *)",
      "Bash(brew *)",
      "Bash(curl *)",
      "Bash(rsync *)",
      "Bash(npx *)",
      "WebSearch",
      "WebFetch(domain:*)"
    ],
    "deny": [
      "Bash(rm -rf *)",
      "Bash(git push --force *)",
      "Bash(chmod -R 777 *)"
    ]
  }
}

Project-specific tools go in the project’s .claude/settings.local.json:

1
2
3
4
5
6
7
8
9
10
11
12
{
  "permissions": {
    "allow": [
      "Bash(bundle *)",
      "Bash(rbenv *)",
      "Bash(gem *)",
      "Bash(ruby *)",
      "Bash(./deploy.sh)",
      "Skill(blog)"
    ]
  }
}

The sandbox handles the actual security enforcement — network egress, filesystem writes. This is where the real protection lives, and it works regardless of whether permission prompts are enabled.

defaultMode: acceptEdits is worth calling out: it auto-approves file reads and edits without needing to list them explicitly. Most of Claude Code’s tool calls are file operations, so this eliminates a huge amount of prompt noise for zero real security cost — the sandbox’s filesystem write restrictions still apply.

How Rules Inherit Across Settings Files

One more thing worth knowing: permission rules are additive across settings scopes, not override-based.

Claude Code loads settings from multiple places — ~/.claude/settings.json (global), .claude/settings.json (project), and .claude/settings.local.json (local). Rules from all of these are merged together. Your global deny list doesn’t disappear just because a project has its own settings file.

Precedence only kicks in when the same rule appears in multiple scopes with conflicting intent. If your global settings deny Bash(rm -rf *) and your project settings allow it, the more specific scope (project/local) wins. But if there’s no conflict, all rules from all scopes apply simultaneously.

In practice, this means:

Scenario Result
Global denies rm -rf *, project has nothing conflicting Global deny is enforced in every project
Global allows Bash(git *), local denies Bash(git push --force *) Both active — git works, force push blocked
Global denies X, local allows X Local allow wins (more specific scope)

This is why putting deny rules in your global settings is the right call — you write them once and they protect you everywhere, without needing to repeat them in each project’s local file.

The Mental Model

Think of it this way:

  Permission Rules OS Sandbox
Controls Does Claude ask first? What can actually execute?
Bypassed by --dangerously-skip-permissions Nothing (OS enforced)
Network WebFetch(domain:*) in allow rules allowedDomains for Bash commands
Filesystem Edit(...) allow/deny rules Hard write restrictions to project dir

The permission rules are about your workflow and your comfort level with prompts. The sandbox is your actual security boundary. Running both gives you the best of both worlds: minimal interruptions for routine tasks, with hard OS-level limits on what can actually go wrong.

That’s the setup I landed on, and it’s the first time my Claude Code config has felt both fully usable and actually secure — not just one or the other.

This post is licensed under CC BY 4.0 by the author.