The runner security model
A coding agent is a program with a shell, network access, and permission to edit files. Running one on your machine is only reasonable if it runs inside a boundary you can describe. This page describes it.
The short version: the operating system enforces the boundary, the runner refuses to start an agent without one, and the agent never sees your home directory, your environment, or the credentials the runner itself uses.
The boundary, per operating system
Section titled “The boundary, per operating system”| Platform | Mechanism | How it is applied |
|---|---|---|
| Linux | Landlock, in the kernel | A small helper applies the ruleset and then becomes the coding agent, so the agent and everything it spawns inherit the restriction |
| macOS | Seatbelt (/usr/bin/sandbox-exec) | The runner writes a per-attempt profile, and sandbox-exec applies it and then becomes the coding agent |
Both are unprivileged. There is nothing to install, no kernel module, no daemon, and no sudo. Both apply the restriction and then execve the agent, which is the property that makes the shape work: the restriction survives becoming another program, so it covers the agent, its shell, its subprocesses, and its tools.
There is one place in the runner that turns a command line into a running agent, and that place applies the boundary. Adapters cannot spawn around it, because an adapter does not start processes itself. An audit of the source fails the build if any new place that starts a process hands it the runner’s environment without being a declared, justified exception.
Fail closed, with no override
Section titled “Fail closed, with no override”The runner does not conclude that a machine is safe from a kernel version, a feature list, or the presence of a helper binary. It proves it, on every start:
- It builds a real, restrictive policy.
- It runs a real child process through it.
- It requires the operating system to deny that child a read the policy never granted, and to allow one it did.
If that check fails, the runner does not run an agent on that machine. It reports why and points you at a managed runner instead. There is no flag, environment variable, or configuration setting that downgrades this to a warning, and there is no degraded mode.
You can run the same check yourself at any time with doctor.
The agent never sees your home directory
Section titled “The agent never sees your home directory”Every coding tool reads its configuration from your home directory before it does anything else: settings, plugins, MCP servers, and hooks. A hook is arbitrary code that the tool runs on your behalf. On your own machine that code is yours, chosen by you, and it has no business executing next to an agent working on a task.
So the runner does not lend the agent your home. Each attempt gets a synthetic home: a fresh directory tree, created for that attempt, holding exactly one thing.
<state>/cc-attempt-<attempt>-<agent>-<nonce>/ mode 0700 home/ the agent's $HOME config/ the agent's tool configuration directories tmp/ the agent's scratch spaceFour properties of that tree are deliberate:
- It is outside the git workspace. Anything inside the workspace could be committed by the agent and pushed to your repository.
- Directories are
0700and staged credential files are0600. On a shared machine the next user is not entitled to either. - The path carries a random value, so nothing can pre-create or symlink the directory an attempt is about to write its credential into.
- Nothing is ever copied back out. The agent may rewrite its own settings, cache, or refreshed token inside that tree; all of it dies with the attempt rather than being promoted into anything the runner trusts.
What an agent can never reach
Section titled “What an agent can never reach”Access is granted, never subtracted. Your real home directory is not granted, so everything in it is unreachable by omission rather than by a rule someone remembered to write:
~/.ssh, so the agent cannot read your keys. The runner’s own git still uses your SSH agent, because the runner is not the agent.- The credential files your cloud and infrastructure tools keep in your home directory.
- The macOS Keychain. On macOS the profile allows a specific list of system services by name, and the Keychain service is deliberately not on it, so the agent cannot ask the Keychain for your secrets even though ordinary system calls work.
- Git’s executable configuration. Git runs code it finds in a repository: hook scripts and the command-valued keys in
config. Those paths are kept out of the agent’s reach, so an agent cannot plant a hook and wait for the runner to run git. See workspaces and git.
The environment allowlist
Section titled “The environment allowlist”The agent does not inherit the runner’s environment. Each agent process gets a built environment, from an allowlist, in this order:
- An execution baseline.
PATH,HOME,USER,LOGNAME,SHELL,TMPDIR,LANG,LC_ALL,LC_CTYPE,TZ,TERM, and the certificate-bundle paths a corporate proxy needs. Paths and locale, nothing more. Nothing whose value is a secret is allowed on this list. - The run’s selected environment. Your project’s deployment values, released for that attempt only, injected into the process and never written into the workspace.
- That adapter’s own credential, and only its own, plus the attempt’s synthetic
HOME,TMPDIR, and configuration paths. This layer wins, so a project environment cannot point the agent back at your real home or substitute a stored key for the released one.
No coding agent receives another one’s key. Everything else is dropped, including the runner’s own control-plane credential, its tenant identity, GITHUB_TOKEN, cloud role credentials, NODE_OPTIONS, and any unrelated API key that happens to be exported in the shell you started the runner from.
Two entries are missing on purpose:
- Proxy variables are not on the baseline, because a proxy URL routinely embeds a username and password.
NODE_OPTIONSand similar are not on the baseline, because they execute code nobody asked for.
What the runner tells the control plane
Section titled “What the runner tells the control plane”The runner reports its containment as exactly two fields: the platform and the tier. Kernel versions, helper paths, profile contents, and the rest of the diagnostic detail stay on your machine and are printed only by doctor, which says so in its own output.
The fleet shows those two fields per runner, so you can see at a glance that every machine taking work is contained:

Where credentials live
Section titled “Where credentials live”- The runner’s pairing credential is one
0600file in~/.commandchain. It is never printed, never a command-line argument, and never written into the service definition. - Coding-agent credentials are stored in your workspace, released to a single attempt, and never written to disk on the runner. A second release for the same attempt is refused. See agent credentials.
- Project environment values follow the same rule: released for one attempt, injected into the process, never written into the workspace.
Next steps
Section titled “Next steps”doctor: run the containment proof yourself.- Agent credentials: capture, release, and revocation.
- Workspaces and git: why the runner pushes and the agent does not.