Codex vs Claude

Below the Prompt: What a Kernel Sandbox Actually Constrains

David Guzenburg/ / 11 min read

It protects your SSH keys. It has nothing to say about whether a change to auth.py is a fix or a backdoor.

sandboxingisolationleast privilegethreat model

Two kinds of no

An agent can be told not to do something, and it can be prevented from doing it. Those are not weaker and stronger versions of the same control; they are different mechanisms with different failure modes, and the difference becomes stark the moment the agent is processing text somebody else wrote.

An instruction — in a system prompt, an instruction file, a tool description — is input to a model that also reads issue comments, dependency READMEs, web pages and log output. A kernel policy is not input to anything. It is a decision made by the operating system before the syscall happens, and no amount of persuasive text in a fetched document changes it.

That is the entire argument for sandboxing below the prompt, and it is a good one. What follows is what the mechanisms actually do, and the specific place where a well-configured sandbox still leaves you exposed.

What the mechanisms are

On macOS, Seatbelt: a policy language applied through sandbox-exec, with a profile matching the selected sandbox mode. On Linux, bwrap together with seccomp — a namespace-based container-style isolation plus a syscall filter. On Windows, WSL2 uses the Linux path and native Windows has its own implementation.

It is worth being precise here because the mechanisms are frequently misreported. Landlock, in particular, gets named in comparisons where the implementation actually uses bwrap and seccomp. The distinction matters if you are reasoning about what is enforced, because the mechanisms have different granularities and different escape surfaces.

What a profile can express

Roughly three things. Which paths are readable and which are writable. Whether the process may reach the network. What system operations are permitted at all — which syscalls, whether new processes can be spawned, whether privileges can be raised.

That vocabulary is coarse on purpose: it is enforceable, and enforceable means checkable without understanding intent. A sandbox cannot express "do not delete anything important." It can express "this directory is read-only," which is the enforceable neighbour of that idea.

Network off by default is half the value

The default in a workspace-write configuration is that network access is turned off, and enabling it is an explicit setting. That single default does more for the realistic threat model than the filesystem rules do, because most of what you are actually worried about — a poisoned dependency, an instruction smuggled into a fetched page, a credential leaving the machine — needs an outbound connection to matter.

It is also the default people disable first, because it breaks package installation. That trade, and the allowlist that lets you keep most of the benefit, is the subject of default-deny egress for agents.

The workspace-write hole

Here is the part worth internalising, because a sandbox produces a strong feeling of safety that its actual policy does not support.

A workspace-write sandbox permits writes inside the workspace. Your workspace contains your source code. Therefore the sandbox permits arbitrary modification of your source code, which is not a limitation of the sandbox — it is the entire purpose of the session. The kernel policy has nothing to say about whether a change to auth.py is a fix or a backdoor.

So the sandbox bounds where damage can occur and says nothing about what kind. Inside the boundary, your controls are review, version control and tests, exactly as they were before. The sandbox protects your SSH keys. It does not protect your codebase.

Two files inside the workspace that are executable

The sharp version of the previous point. .git/hooks/ is inside your repository, and the files in it are scripts that run on ordinary git operations. A workspace-write policy permits writing there. So does anything else that can write to your working directory.

The same is true of build configuration that executes during install — lifecycle scripts in a package manifest, a conftest.py, a Makefile, a task definition your editor runs on open. Each is a file in the workspace whose content becomes execution, without anyone typing a command.

Check the hooks directory, not just the diff

.git/hooks/ is not tracked, so a change there appears in no diff, no pull request and no git status. If you take one concrete action from this article, make it a periodic check that your hooks directory contains only what you put there.

What still gets out

Whatever you granted. A mounted socket that talks to a privileged daemon is a path out of the sandbox regardless of the filesystem policy, because the thing on the other end is not sandboxed. A helper binary running outside the sandbox that the sandboxed process can ask to do things. A shared directory that another process watches and acts on.

And the escape hatch: approving a single command to run outside the sandbox. That mechanism exists because it has to, and it means the effective policy is the configured one plus every exception a tired person granted at five o'clock, which is the argument in approval fatigue arriving from a different direction.

Test the sandbox rather than trusting it

Sandbox configuration fails quietly. A misconfigured profile does not error; it permits. So the only way to know your policy is in effect is to attempt the things it should refuse and check that they fail.

#!/usr/bin/env bash
# Run INSIDE the agent's sandbox. Every check must FAIL to pass.
fails=0
check() {  # check "name" "command"
  if eval "$2" >/dev/null 2>&1; then
    echo "PERMITTED (should be denied): $1"; fails=1
  else
    echo "denied ok: $1"
  fi
}

check "read ssh private key"   'cat ~/.ssh/id_ed25519'
check "read aws credentials"   'cat ~/.aws/credentials'
check "write outside workspace" 'touch ~/sandbox-escape-probe'
check "write to home dotfile"  'echo x >> ~/.zshrc'
check "outbound DNS"           'getent hosts example.com'
check "outbound HTTPS"         'curl -sS --max-time 4 https://example.com'
check "spawn privileged"       'sudo -n true'

exit $fails

Run it once when you set the sandbox up, and again after any change to the configuration or an update to the tooling. It takes two seconds and it is the difference between believing you have a policy and knowing it.

Sandbox, container, virtual machine

Three levels, and the choice is about what you are willing to lose. A kernel sandbox keeps your actual environment — your tools, your running services, your authenticated sessions — and constrains paths, network and syscalls around it. A container gives you a reproducible filesystem and loses your environment. A virtual machine gives you a kernel boundary and loses more.

The sandbox is the only one of the three that is nearly free, which is why it should be on by default even when you also use the others. The fuller comparison of what each level costs is in sandboxing agent environments; the point here is that these compose rather than compete.

What breaks, honestly

Package installs, because they need the network. Tools that write to a cache in your home directory. Anything resolving a symlink that points outside the workspace. Test suites that reach a service on another host. Language servers and version managers that expect to write to their own directories.

Each has a fix — a permitted cache path, an egress allowlist entry, a pre-seeded dependency install before the sandboxed session — and the work of applying them is the actual cost of running sandboxed. It is a half-day once, and then it is invisible.

Relying on operating system facilities you do not control

One caution worth stating. These mechanisms are provided by the platform, and platforms deprecate things. A sandboxing approach that depends on a facility the vendor considers legacy is a dependency with an expiry date you do not set.

The practical response is not to avoid them but to layer: the kernel sandbox is one control among several, and the others — a deny list, egress restrictions, credentials that are not present in the environment, review before merge — should each be sufficient to prevent the worst outcome on their own. That way an operating system release cannot silently remove your only defence.

Reading your own profile is worth the twenty minutes

Sandbox profiles are usually treated as opaque configuration selected by a mode name. They are text, they are short, and reading the one you are running tells you things no summary will: which paths are exempted, whether temporary directories are writable, what happens to symlinks, whether process spawning is constrained at all.

The exemptions are the interesting part. Every practical profile carries a handful, because a completely sealed process cannot run a compiler. Those exemptions are your real attack surface, and they are individually reasonable and collectively worth knowing.

Temporary directories are the usual soft spot

Almost every profile permits writes to a temporary directory, because almost every toolchain needs one. That is fine for the intended use and it means the sandbox permits writing an executable file to a location that is also readable by everything else running as you.

On its own that is harmless — writing a file is not running it. It becomes relevant in combination: a permitted temporary write plus any other process that executes things from there, plus a shell that has that directory on its path for some historical reason. Composition is where sandbox assumptions go wrong, and the way to find those combinations is to enumerate exemptions rather than to reason about the policy as a whole.

The sandbox and the model are solving different problems

A useful framing when deciding how much to invest. The kernel policy defends against the agent doing something outside its remit — reaching a credential, writing to a system path, phoning home. Review, tests and version control defend against the agent doing the wrong thing inside its remit — a broken change, a subtle regression, a backdoor in a file it was supposed to edit.

Neither substitutes for the other, and teams reliably over-invest in the first because it is concrete and configurable, while the second is organisational and boring. If your agent setup has a carefully tuned sandbox and no requirement that changes be reviewed before merge, the effort went to the smaller of the two risks.

What to do about the untracked-hooks problem

Since it is the sharpest specific hole in a workspace-write policy, it deserves a concrete answer rather than a warning. Three options, in increasing order of effort.

Set core.hooksPath to a tracked directory, so the hooks that run are the hooks in version control, and a write to .git/hooks/ does nothing. Or add the hooks directory to the sandbox's read-only paths, so the policy refuses the write outright. Or run a periodic check that lists the directory and compares it against a recorded hash, which catches the case where the other two were bypassed.

The first is the one I would do: it costs one command, it is a configuration people already understand, and it converts an invisible execution path into a tracked one that shows up in review like everything else.

Verify after every update

The last habit, and the one most likely to be skipped. Sandbox behaviour changes with tooling updates, operating system releases and configuration edits made for unrelated reasons. A policy that was correct in June is not self-evidently correct in September.

Running the self-test after an update takes two seconds and is the only thing standing between "we sandbox our agents" as a description of your setup and as a description of your intentions. Wire it into whatever you already run after upgrading, so the check happens without anyone remembering to do it.

Takeaway

A kernel policy is categorically stronger than an instruction because injected text cannot lift it, and Seatbelt on macOS or bwrap plus seccomp on Linux is nearly free to enable. But it constrains location, not intent: a workspace-write policy permits every change inside your repository, including .git/hooks/, which is untracked and appears in no diff. Verify the policy by attempting what it should refuse, keep network off by default, and treat the sandbox as one layer rather than the answer.

Keep reading
Codex vs Claude

Default-Deny Egress: The Control You Turn Off in the First Hour

Outbound network access is the highest-value restriction on an agent and the one that breaks npm install. Pre-seeding dependencies, hostname filtering at a proxy, why a credential fits in a query string, and verifying the denial by IP.

Codex vs Claude

Kernel Sandbox or Application Permission Layer?

Kernel enforcement is the stronger last line for filesystem and process boundaries; application policy is often more expressive and easier to tailor. The.

Codex vs Claude

Approval Fatigue: The Control Degrades Every Time You Use It

Interactive permission prompts spend a consumable resource. Why the count matters more than the wording, why deny lists beat allow lists, and how to make destructive commands break the rhythm instead of matching it.

Codex vs Claude

What the Agent Inherits: Sessions You Are Already Logged Into

Credential discussions focus on secrets in files. On a developer machine most access is an authenticated session an agent can simply use: cloud CLIs, the current kubectl context, a forwarded SSH agent. An inventory script and what to make absent by default.

← Local Execution: It Worked in the Session Is the New Works on My Machine  ·  Layered Architecture or Functional Single Surface? →

All codex vs claude articles  ·  Every article