Agent Safety24 min read

The Kernel Said No: How to Actually Contain AI Coding Agents on macOS

What the Matt Shumer incident reveals about agent trust boundaries, how macOS Seatbelt works at the kernel, and how to lock Claude Code, Codex, Cursor, and OpenCode to your work directory in under five minutes.

What the Matt Shumer incident reveals about agent trust boundaries, how macOS Seatbelt works at the kernel, and how to lock Claude Code, Codex, Cursor, and OpenCode to your work directory in under five minutes.

Product setup and fail-closed design: docs.clawql.com/getting-started/local-agent-sandbox · ADR 0008. This pairs with the kernel and sidecar layers in Hardened Agentic Stack and the OpenAI/Hugging Face four-failures incident.

What Happened on July 10, 2026

At 12:03 PM on July 10, 2026, Matt Shumer posted this on X:

“GPT-5.6-Sol just accidentally deleted almost ALL of my Mac’s files.”

The model’s own trace explained what happened:

“I caused a serious local data-loss incident. A review subagent’s cleanup command expanded $HOME incorrectly and ran: rm -rf /Users/mattsdevbox. I found and killed the still-running process, but material deletion occurred.”

The post got 2.7 million views in 24 hours.

Read that trace carefully. The model is describing what it did in past tense. It caught the error itself. It killed the process itself. The deletion was already underway before any of that happened — because there was nothing between the agent deciding to run a command and the command actually executing.

No sandbox. No kernel-level containment. No path restriction. The agent had full write access to the entire home directory, so when $HOME expanded incorrectly and the cleanup command ran, there was nothing to stop it.

This is not a model quality problem. GPT-5.6-Sol presumably didn’t intend to delete the home directory. Variable expansion bugs happen. Shell commands run with whatever permissions the process has. The failure was architectural: an AI coding agent was running with the same filesystem access as the user who launched it, and there was no enforcement layer between “agent decides to run command” and “command executes.”


Why This Keeps Happening

The instinct after an incident like this is to blame the model. Make the model smarter. Add a system prompt that says “be careful with destructive commands.” Ask the model to confirm before running rm.

These interventions are not wrong. They’re just insufficient, for a structural reason.

A prompt is not a security control.

A prompt is a request. A well-aligned model will usually honor it. A model that misunderstands context, hallucinates a variable value, or follows a chain of reasoning that seems internally consistent but produces a destructive outcome, will not. You cannot prompt-engineer your way to a security guarantee, because the model is the entity you’re trying to constrain, and prompts are advice to that entity, not enforcement against it.

The correct mental model: prompts are like asking an employee to be careful. Seatbelt profiles are like a physical lock on the filing cabinet. Both have a role. Only one is a security control.

The deeper issue is that AI coding agents in 2026 commonly operate with three capabilities that combine badly without containment:

Subagent spawning. The primary agent spawns secondary agents — review agents, test agents, cleanup agents — that run commands the primary agent didn’t directly write. The Matt Shumer incident was specifically a review subagent that ran the destructive command, not the primary agent. The primary agent was as surprised as the user.

Shell access. Agents with bash/shell tool access can run arbitrary commands with the permissions of the launching user. This is necessary for coding work. It’s also the attack surface.

Variable expansion in generated code. Agents write shell commands using variables. Variable expansion bugs in shell are famously subtle — the $HOME mis-expansion in the Shumer incident is a classic class of bug that experienced shell programmers hit. Agents aren’t immune to generating code with the same bugs.

The intersection of these three: a subagent, that the primary agent spawned and doesn’t fully control, that has full filesystem access, that generates a shell command with a variable expansion bug. That’s the Shumer incident. It will happen again on whatever model is current at the time, because the architectural vulnerability is in the environment, not the model.


What macOS Seatbelt Actually Is

Before getting to the solution, it’s worth understanding the tool.

macOS Seatbelt is a mandatory access control framework built into the macOS kernel. It’s been part of macOS since 10.5 Leopard. It runs at the kernel level — not in userspace, not in a daemon you can kill, not as a launchd item that can be disabled. When Seatbelt denies an operation, the operation fails before the syscall completes. The process trying to perform the operation receives an error. It cannot work around this without root access to disable the policy.

The tool for using Seatbelt from the command line is sandbox-exec. You provide it a profile — a small declarative language that specifies what operations are allowed or denied — and a command to run, and it executes that command under the constraints in the profile.

A minimal profile looks like this:

(version 1)
(allow default)          ; allow everything by default
(deny file-write*)       ; then deny all file writes
(allow file-write*       ; then re-allow writes to specific paths
  (subpath "/tmp")
  (subpath "/Users/daniel/company-repos"))

The evaluation order matters: rules are evaluated in order, and the last matching rule wins. The pattern above — allow default, deny writes broadly, re-allow writes to specific paths — means any file write outside the allowed paths is denied at the kernel level, regardless of what the process tries to do.

This is the key property: Seatbelt enforcement cannot be bypassed by the sandboxed process. The process has no mechanism to modify its own Seatbelt policy after launch. It can’t call an API to expand its permissions. It can’t spawn a child process that escapes the policy (child processes inherit the Seatbelt policy unless explicitly configured otherwise). If the policy says “deny file-write* outside /Users/daniel/company-repos,” then a rm -rf /Users/daniel issued by any process — whether the primary agent, a subagent, a subprocess spawned by a subagent, or any other descendant — will fail.

Not “might fail.” Not “will fail unless the model finds a workaround.” Will fail. The kernel denied it.


What the Matt Shumer Incident Looks Like With Seatbelt

Let’s trace through the same incident with a Seatbelt profile active.

The review subagent generates a cleanup command. Variable expansion produces rm -rf /Users/mattsdevbox instead of the intended path. The command runs.

The rm process calls unlink() (or equivalent) on the first file in /Users/mattsdevbox.

The kernel checks the Seatbelt policy for the process. The policy says (deny file-write*) with re-allowances only for /tmp and the active work directory. /Users/mattsdevbox does not match any allowed path.

The kernel returns EPERM — permission denied — to the rm process. rm logs an error and either stops or continues to the next file, which will also fail.

No files are deleted. The agent’s trace records a permission error. The primary agent sees the error and can report it to the user.

The user sees: “rm failed — permission denied.”

The user does not see: 2.7 million views on a post about losing their home directory.

The critical property: this happens before any deletion occurs. Not after. Not while trying to recover. The kernel intercepts the syscall before the filesystem is touched. That’s what “kernel-level containment” means in practice.


Setting Up Seatbelt for AI Coding Agents on macOS

There are two paths: manual configuration if you want to understand exactly what’s happening, and clawql sandbox init if you want something production-ready that handles all four major harnesses.

The Manual Path (So You Understand the Tool)

Step 1: Create your work directory structure

Pick one directory where all your company repositories will live. This is the path you’ll grant write access to.

mkdir -p ~/company-work/repos

From now on, always clone company repositories here:

cd ~/company-work/repos
git clone [email protected]:your-org/your-project.git

Step 2: Write a Seatbelt profile

Create ~/.sandbox-profiles/agent.sb:

(version 1)

; Start by allowing everything (default-allow model)
(allow default)

; Deny all file writes globally
(deny file-write*)

; Re-allow writes to safe locations
(allow file-write*
  (subpath "/tmp")
  (subpath "/var/folders")              ; macOS temp files
  (subpath (param "WORK_DIR"))          ; your active project
  (subpath (param "CLAWQL_DIR")))       ; ~/.ClawQL for memory/vault

; Explicitly deny reads from sensitive locations
; (these override the default-allow at the top)
(deny file-read*
  (subpath (param "HOME_SSH"))          ; ~/.ssh
  (subpath (param "HOME_AWS"))          ; ~/.aws
  (subpath (param "HOME_CONFIG")))      ; ~/.config (contains tokens for many tools)

A few things to note about this profile:

The parameterized values ((param "WORK_DIR")) are filled in at launch time via -D flags on sandbox-exec. This means one profile can work for any project — you pass the current project directory as a parameter rather than hardcoding it.

The (deny file-read*) rules for ~/.ssh, ~/.aws, and ~/.config are important. An agent that can’t write your home directory can still read your SSH keys and AWS credentials. Explicit read denials on sensitive directories prevent exfiltration even if the agent is manipulated into trying to access them.

/var/folders is macOS’s system temp directory for user-specific temp files. Various tools and the runtime itself need to write here — blocking it breaks things.

Step 3: Launch your agent through sandbox-exec

For Claude Code:

sandbox-exec -f ~/.sandbox-profiles/agent.sb \
  -D WORK_DIR="$PWD" \
  -D CLAWQL_DIR="$HOME/.ClawQL" \
  -D HOME_SSH="$HOME/.ssh" \
  -D HOME_AWS="$HOME/.aws" \
  -D HOME_CONFIG="$HOME/.config" \
  -- claude

For Codex:

sandbox-exec -f ~/.sandbox-profiles/agent.sb \
  -D WORK_DIR="$PWD" \
  -D CLAWQL_DIR="$HOME/.ClawQL" \
  -D HOME_SSH="$HOME/.ssh" \
  -D HOME_AWS="$HOME/.aws" \
  -D HOME_CONFIG="$HOME/.config" \
  -- codex

Create shell aliases for convenience:

# Add to ~/.zshrc or ~/.bashrc
alias claude-safe='sandbox-exec -f ~/.sandbox-profiles/agent.sb \
  -D WORK_DIR="$PWD" \
  -D CLAWQL_DIR="$HOME/.ClawQL" \
  -D HOME_SSH="$HOME/.ssh" \
  -D HOME_AWS="$HOME/.aws" \
  -D HOME_CONFIG="$HOME/.config" \
  -- claude'

alias codex-safe='sandbox-exec -f ~/.sandbox-profiles/agent.sb \
  -D WORK_DIR="$PWD" \
  -D CLAWQL_DIR="$HOME/.ClawQL" \
  -D HOME_SSH="$HOME/.ssh" \
  -D HOME_AWS="$HOME/.aws" \
  -D HOME_CONFIG="$HOME/.config" \
  -- codex'

Step 4: Verify it works

From inside your project directory, run the agent and ask it to try writing outside its allowed path:

cd ~/company-work/repos/my-project
claude-safe
# Inside Claude: "create a file at ~/test.txt"
# Expected result: permission denied error

If it fails with a permission error, containment is working. If the file is created, something is wrong with the profile — check that sandbox-exec is actually being invoked and that the profile path is correct.

Step 5: Add Claude Code’s native sandbox as a second layer

Claude Code has built-in Seatbelt support via /sandbox or settings.json. For Claude specifically, you can get two layers of containment — the outer sandbox-exec wrapper and Claude’s own internal sandbox:

// ~/.claude/settings.json
{
  "sandbox": {
    "enabled": true,
    "allowedPaths": ["~/company-work/repos"],
    "deniedPaths": ["~/.ssh", "~/.aws", "~/.config", "~/Documents", "~/Desktop", "~/Downloads"]
  }
}

With both layers active: if the outer sandbox-exec profile has a gap, Claude’s own sandbox catches it. If Claude’s own sandbox has a gap, the outer sandbox-exec profile catches it. Defense in depth at the laptop level, using the same principle as Kata containers + Istio at the enterprise level.


The ClawQL Path (Production-Ready, All Four Harnesses)

clawql-sandbox automates all of the above and extends it to Claude Code, Codex, Cursor, and OpenCode with a single command:

# Install ClawQL
curl -fsSL https://clawql.com/install | bash

# Generate Seatbelt profiles for all four harnesses
# and write Claude's settings.json
clawql sandbox init

# Verify containment is actually working
clawql sandbox verify

# Launch any harness through its sandbox wrapper
clawql claude      # sandbox-exec wrapper → Claude Code
clawql codex       # sandbox-exec wrapper → Codex
clawql cursor      # sandbox-exec wrapper → Cursor
clawql opencode    # sandbox-exec wrapper → OpenCode

What clawql sandbox init does:

  • Generates ~/.ClawQL/sandbox/claude.sb, codex.sb, cursor.sb, and opencode.sb — one parameterized profile per harness
  • Writes ~/.claude/settings.json with allowedPaths and deniedPaths configured (Claude double-layer containment)
  • Sets failClosed: true — if sandbox-exec is missing or clawql sandbox verify fails, the harness launch aborts rather than proceeding unsandboxed

The fail-closed behavior is the critical design decision. The alternative — fall back to launching the harness without sandboxing if something goes wrong — means that any problem with the sandbox setup silently removes your protection. clawql sandbox refuses to silently degrade. If containment can’t be established, the agent doesn’t launch. You get an error. You fix the error. Then you launch.

The command surface:

clawql sandbox init                      # generate profiles + configure Claude
clawql sandbox status                    # show per-harness profile paths
clawql sandbox verify                    # run kernel-level containment probes
clawql sandbox edit --harness claude     # open claude.sb in $EDITOR
clawql doctor --smoke                    # includes sandbox verify when enabled

The generated profile template:

(version 1)
(allow default)
(deny file-write*)
(allow file-write*
  (subpath "/tmp")
  (subpath (param "WORK_DIR"))
  (subpath (param "CLAWQL_DIR")))
(deny file-read*
  (subpath (param "HOME_SSH"))
  (subpath (param "HOME_AWS"))
  (subpath (param "HOME_CONFIG")))

Identical in structure to the manual profile above. You can inspect and edit it with clawql sandbox edit --harness <name>.


The Escalation Ladder: How Much Containment Do You Need?

Seatbelt is the right tool for most developers most of the time. There’s a full escalation path for higher-risk scenarios:

LevelToolUse When
1 — macOS Seatbeltclawql sandbox initDaily coding work on macOS. Covers the Shumer incident class.
2 — sandbox_exec MCP toolCLAWQL_ENABLE_SANDBOX=1Agent-generated code snippets that need in-process execution isolation.
3 — Kata ContainersHelm sandboxKataEnterprise Kubernetes workloads. VM-level isolation for agent pods.
4 — UTM Virtual MachineSeparate macOS/Linux VMComputer Use, screen control features, or any task where you want the agent physically isolated from the host machine.

For most developers, Level 1 is the answer. The setup takes five minutes and prevents the entire class of incident Matt Shumer experienced.

For teams using Claude’s Computer Use features — where the agent can control the screen, not just the filesystem — Level 4 is the right answer. Computer Use can click UI elements, interact with applications, and perform actions that Seatbelt can’t fully contain because they operate above the filesystem. A VM physically separates the agent’s actions from your host machine: even if the agent does something destructive inside the VM, your host is untouched.

UTM is the recommended tool for macOS VM setup:

  1. Install UTM from the Mac App Store or utmapp.github.io
  2. Create a new macOS or Linux VM
  3. In VM settings, share only ~/company-work/repos — not your full home directory
  4. Install the agent inside the VM
  5. Use the VM for any Computer Use work

The overhead is manageable on modern Apple Silicon. The isolation is complete.


What Seatbelt Doesn’t Protect Against

Being clear about limitations is as important as being clear about capabilities.

Network access is unrestricted by default. The profile above limits filesystem access. It does not limit network access. An agent that is manipulated into exfiltrating data can still make outbound network requests. If you need network containment — restricting which hosts the agent can connect to — that requires additional profile rules using (deny network-outbound) with re-allowances for specific hosts. This is more complex to configure because it requires knowing which hosts your agent legitimately needs to reach.

Seatbelt doesn’t protect against a malicious model. Seatbelt constrains what the process can do. It doesn’t constrain what the model can say. If an agent is manipulated into exfiltrating data through a channel that’s allowed — writing to an allowed file path and then that file being served by a legitimate process, for example — Seatbelt won’t stop that. Defense in depth means network monitoring, output logging, and Langfuse agent tracing complement the filesystem containment.

The profile needs to match your actual workflow. If your project needs to write to paths outside the configured WORK_DIR — installing global packages, writing to other user directories — those operations will fail. You’ll need to either add those paths to the allowed list or restructure your workflow to keep writes local to the project. Most legitimate coding work stays within the project directory, but there are exceptions worth knowing about before you discover them mid-session.

sandbox-exec is macOS only. This approach is specific to macOS. Linux has analogous mechanisms (seccomp, namespaces, cgroups, bubblewrap) but they require different configuration and tooling. Windows has different isolation primitives entirely. If your team uses multiple platforms, you need platform-specific solutions.

The outer sandbox doesn’t protect against bugs in sandbox-exec itself. Seatbelt has been in production since macOS 10.5. It’s been audited extensively. But no security tool is perfect. For highest-risk work, the VM is the right answer — it doesn’t rely on the correctness of any macOS security primitive beyond the VM hypervisor.


Why “Policies” and “Recommendations” Aren’t Enough

There’s a category of advice that circulates after incidents like this: “add a system prompt that tells the agent to be careful,” “require confirmation before destructive commands,” “use a model with better safety training.”

These are all reasonable things to do. None of them are security controls.

The distinction matters because the failure mode is not “the agent decided to do something destructive.” The failure mode in the Shumer incident was “a subagent generated a command with a variable expansion bug.” The primary agent didn’t intend the destructive outcome. A system prompt asking the primary agent to be careful has no effect on a variable expansion bug in code generated by a subagent.

The question to ask about any safeguard: does it prevent the bad outcome even if the model behaves unexpectedly? A system prompt doesn’t. Seatbelt does. The filesystem write either happens or it doesn’t, based on what the kernel decides, not based on what the model intended.

This is the same principle that makes security controls in traditional software security meaningful: defense in depth means each layer is independently effective, not dependent on all other layers working correctly. Seatbelt is effective even when the model makes a mistake, the subagent behaves unexpectedly, and the user doesn’t notice in time to intervene.


The Five-Minute Setup

If you want to be protected right now, without reading the rest of the post:

# Option 1: ClawQL (all four harnesses, fail-closed)
curl -fsSL https://clawql.com/install | bash
clawql sandbox init
clawql sandbox verify
# Use: clawql claude / clawql codex / clawql cursor / clawql opencode

# Option 2: Manual (one harness, you control the profile)
mkdir -p ~/.sandbox-profiles
cat > ~/.sandbox-profiles/agent.sb << 'EOF'
(version 1)
(allow default)
(deny file-write*)
(allow file-write*
  (subpath "/tmp")
  (subpath "/var/folders")
  (subpath (param "WORK_DIR")))
(deny file-read*
  (subpath (param "HOME_SSH"))
  (subpath (param "HOME_AWS"))
  (subpath (param "HOME_CONFIG")))
EOF

# Add to ~/.zshrc:
alias claude-safe='sandbox-exec -f ~/.sandbox-profiles/agent.sb \
  -D WORK_DIR="$PWD" \
  -D HOME_SSH="$HOME/.ssh" \
  -D HOME_AWS="$HOME/.aws" \
  -D HOME_CONFIG="$HOME/.config" \
  -- claude'

Run source ~/.zshrc (or open a new terminal), then use claude-safe instead of claude.

Test it:

cd ~/company-work/repos/my-project
claude-safe
# Ask the agent: "create a file at ~/test-outside-sandbox.txt"
# Expected: permission denied
# If the file is created: something is wrong, check your profile

If the test works, you’re protected against the Shumer class of incident. The next rm -rf $HOME expansion bug gets caught at the kernel, before any files are deleted.


Conclusion

The Matt Shumer incident wasn’t a model failure. It was an environment failure. The model didn’t have a security vulnerability. The environment that ran the model had no security boundary between “agent decides to run a command” and “command executes with full user permissions.”

macOS Seatbelt closes that gap. It’s been in the kernel since 2007. It’s what App Store apps use for sandboxing. It’s what iOS uses for app isolation. It’s battle-tested, low-overhead, and takes five minutes to apply to an AI coding agent.

The right mental model: prompts are advice. Seatbelt is a lock. You need both, for different reasons. Prompts make the agent more likely to do the right thing. Seatbelt ensures that when the agent does the wrong thing — through a bug, through a misunderstanding, through a subagent operating outside the primary agent’s direct control — the damage is bounded.

The kernel said no. That’s the only answer that reliably holds.


The clawql sandbox module, including generated Seatbelt profiles for all four harnesses and fail-closed launch wrappers, is available at docs.clawql.com/getting-started/local-agent-sandbox. ADR 0008 documents the design decisions behind the fail-closed behavior.

About the author

Daniel Smith builds ClawQL, an agent operating system for token-efficient discovery and execution over APIs — with observability, hardened tool boundaries, and production routing for LLM workloads. He writes here about the systems problems behind shipping agents.