Harness and tool boundaries
For: cluster operators and plugin authors.
A session's tools and its harness
A session's tools run as the same user as the harness that asked about them, and at tier 1 in the same sandbox, so the sandbox's walls do not stand between them. A call that the job refused must stay refused, and a session's answer must be the harness's, so this boundary holds on its own:
- The control channel has no path. Permission requests and their answers, a session's events and its final result travel on the streams between the runtime and the session's process: a harness, an ACP agent, or a remote session's adapter and executor. On Linux each of these streams is one end of a socket pair, and so is each stream between the Claude harness and its CLI, and between a remote session's executor and its shell.
A pipe can be opened again through/proc/<pid>/fdby any process of the same user, and whatever it reads the rightful reader never sees. A socket cannot be opened that way and a socket pair has no address. - Nothing the session wrote runs inside them. A harness starts in the session's work folder, and its home and state are the session's too, so anything there is what its tools, a repository or an unpacked archive put there. The runtime starts a Python harness, a remote session's executor and a server in a session or a job's folder with
-Pand-s, so that none of those folders is on its import path. The Claude harness takes no user or project settings, so what they write under.claudeor in.mcp.jsonchanges nothing when a session starts again. - No descriptor can be taken. The seccomp filter refuses
pidfd_getfd,ptraceandprocess_vm_readv/writev. - Memory cannot be written through
/proc. A session's/procis read-only, so/proc/<pid>/memdoes not open for writing, whatever the host's Yama setting. The Claude harness and the sandbox's bridge are not dumpable, so their descriptors, environment and memory stay closed to the session's tools, though/procstill shows such things as their command lines. - A debugger stays below its harness. A session with
sandbox: {ptrace: true}keeps a writable/procand the tracing calls, which only the kernel then holds to a process's own descendants. It runs only wherekernel.yama.ptrace_scopeis 1 or more, and is refused anywhere else, a kernel without Yama included. - The CLI takes no message from them. The Claude Code CLI listens on a socket for other sessions, which its tools can reach. Every session sets it to refuse what arrives there.
The seccomp filter, the read-only /proc and the rule for debuggers are tier 1's. A session at tier 0, on a node a tenant admin opted out, keeps only what does not depend on the sandbox: the socket streams, and the Claude harness that is not dumpable and refuses those messages. On such a node with kernel.yama.ptrace_scope at 0, not even the streams stand against a process that can trace: a tool can trace a dumpable part such as the CLI, and hold its hook channel through it.
What a session's tools can still do:
- End their own session. They can kill or stop the harness. The session then fails: a harness that ends without its own final result never counts as a success.
- Read what the session already has. The environment of a process that is dumpable, such as the CLI, holds the session's own key and settings. Where
kernel.yama.ptrace_scopeis 0, they can also read that process's memory, though not write it. - Reach what shares their sandbox. An MCP server the session runs itself is in the sandbox with them: they can start or call it without asking. Give a session a plugin's server through the node when that matters.
- Change a later call of their own shell. A remote session's executor keeps one shell between calls, as a terminal does, and each call runs inside it. So a command the job allowed can leave a trap or a function in the shell, or hand its streams to a program it starts, and a later call may then do or say something else, or never run and still report an outcome.
A program reaches those streams only if such a command gave them to it, though one that kept the shell's output can add to what a later call says.
Treat what a shell reports as output of the session's tools. Nothing the job refused gets that far: the runtime decides each call before the executor sees it, and no permission travels on those streams. - Reach a third-party agent's insides. The runtime holds the first stream of an ACP agent. Where the agent is several programs, or talks to itself over a local port, what travels between its parts is that program's, and a tool in its sandbox may reach it. A session that must hold against its own shell should run on a harness whose parts are known, and the agent's asks are checks beside the sandbox, as its notes on asking say.
A harness you write yourself gets the first stream from the runtime. What it starts must not inherit its standard streams, and what it keeps between its own parts should be a socket pair, not a pipe. The runtime closes the imports of a command that names Python itself. One that goes through a shell or a launcher such as uv run gets no such help, and must keep the work folder out of what it loads on its own, or a file there runs with its channel.
macOS has no /proc, and /dev/fd names a process's own descriptors only, so a session's streams stay pipes there. Windows nodes keep pipes too, and no work that needs a sandbox runs on them. Under gVisor stdin and stdout are sockets and stderr stays a pipe to the node, which carries only what goes to the log and a failure's text, and the Sentry applies its own rule for who may trace whom; its /proc stays writable, and this has not been run under gVisor yet.