Below the Prompt: What a Kernel Sandbox Actually Constrains
It protects your SSH keys. It has nothing to say about whether a change to auth.py is a fix or a backdoor.
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.
.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.
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.