Isolating agent runs on a device
What an agent running on your device can reach, and the three shipped ways to tighten it — a dedicated agent OS user, per-stage containers, or running the whole device server in a container.
Every task gets its own sandbox — an isolated git worktree and branch, so parallel tasks never collide with each other (see Core concepts). That isolates tasks from each other. It says nothing about what an agent can reach on the device itself — your home directory, SSH keys, other checkouts on the same machine. This page covers that boundary: what’s exposed by default, and the three ways to shrink it.
The default posture
Unless you set up one of the options below, an agent stage runs as a plain host process — the same OS user that started ch device-server — with its sandbox’s worktree as its working directory. It can read and write anything that user can: your home directory, credentials, other repositories checked out on the same box, all of it. Nothing about the sandbox model restricts this; the worktree isolates tasks from each other, not the agent from the device.
What you get with no setup: push isolation on non-writable stages
One protection already applies out of the box, on every device, in every execution mode. A stage whose Writable switch is off — typically planning and review stages — has the push credential stripped from its own environment: it can commit locally, but the stage process itself isn’t handed a way to push to your shared remote. Writable stages, the ones that actually ship code, get the credential. See Writable under Customising task types and workflows for where that switch lives and what it otherwise controls.
Know the default here, because it’s the permissive one: if a device has no dedicated agent user configured (Option 1 below) and a push credential is still readable by whichever account runs the stage, a non-writable stage runs anyway — with the credential stripped from its own environment, but without refusing to start. To make this fail closed instead — refuse to run a non-writable stage at all while a push credential is reachable and no dedicated agent user is configured — set CH_ALLOW_UNISOLATED_PLANNING=0 in the device server’s environment. It accepts the same values as other CodeHerder boolean settings: 1/true/yes/on and 0/false/no/off.
Option 1: a dedicated agent OS user
The lightest way to separate agents from your own account. Run:
ch device setup-agent-user | sudo sh
setup-agent-user only prints a shell script — it never runs anything itself; piping it to sudo sh is what applies it. It supports Linux and macOS (any other OS refuses with an unsupported-OS error).
Flags:
--user <name>— the agent account to create (defaultch-agent).--group <name>— a shared group used to grant the setup (defaultch-agents).--ch-path <path>— path to thechbinary the sudoers rule may run (defaults to the one you’re currently running).
The generated script creates a low-privilege, no-login system user and the shared group, adds your own account to that group, and installs a passwordless sudo rule that lets the device server start agent processes as that user — nothing broader.
Finish the setup (the script prints these same steps when you run it):
- Log out and back in — or restart
ch device-server— so your new group membership takes effect. - Set
CH_AGENT_USER=ch-agent(or whatever you passed to--user) in the device server’s environment. If you also passed a non-default--group, setCH_AGENT_GROUPto match. - Restart
ch device-server.
Once configured, every agent stage runs as that dedicated user instead of your own account — it cannot read your home directory, your SSH keys, or anything else scoped to you. See Running the device server as a service for where to add an environment variable under systemd or launchd.
Option 2: a fresh container per stage
ch device-server --docker-executor
Environment equivalent: CH_DOCKER_EXECUTOR=1. Off by default — a device runs stages as host processes unless you opt in. Requires Docker installed and running on the device; the stage image is fetched automatically using the device’s own CodeHerder credential, so there’s nothing to build or configure yourself.
With this on, every stage runs in its own container, created fresh and destroyed the moment the stage ends. Only the task’s sandbox — its worktree and the git data it needs — is mounted in; your home directory, SSH keys, and the rest of the machine are not reachable from inside.
Hardening applied to every stage’s container:
- Runs as a non-root user.
- Every extra Linux capability is dropped, and privilege escalation is disabled (
no-new-privileges). - Capped on memory (4 GB), CPU (2 cores), and process count (512) by default.
- The container’s own filesystem is writable — that’s separate from the Writable switch above, which governs whether the stage can push, not what it can write to disk.
Outbound network is filtered on by default: every stage container is routed through a shared proxy on an internal Docker network. With filtering on, the public internet is reachable over HTTP and HTTPS — it is not an allowlist of approved destinations — but requests to private network ranges, loopback and link-local addresses, and cloud-metadata endpoints are refused, which is what stops a stage reaching your internal network. --stage-egress=false turns the filtering off entirely, including those refusals — the stage container isn’t placed on the internal network at all, so private ranges, loopback, and cloud metadata are reachable again. There is no environment-variable equivalent for this flag; turning it off is not recommended.
--docker-executor and --docker (below) are mutually exclusive — passing both is a startup error. --docker puts the whole device server inside one container; --docker-executor puts each stage in its own. Pick one.
Once enabled, the device’s Health snapshot grows a Docker ready check that confirms Docker is reachable and the stage image is available — it blocks new work on that device if it fails. See Device health and readiness checks in Managing your devices. Live terminals and dropping into a running session work the same way in this mode — see Following a live agent session.
A stage doesn’t have to run the device’s default image, either — see Running a stage in your own container image for naming your own image per stage and preparing it with a setup script.
Option 3: run the whole device server in a container
ch device-server --docker
This isolates the device server process itself — and everything it spawns — from the host machine: your home directory, SSH keys, and other credentials on your machine are not accessible from inside the container. It’s coarser than Option 2: one container holds the whole server and every stage it runs, rather than a fresh container per stage. See Docker container (host-isolated) in Running the device server as a service for setup, requirements, and how it interacts with session storage.
Choosing between the options
- Nothing configured: you already have push isolation on non-writable stages (see above) — consider
CH_ALLOW_UNISOLATED_PLANNING=0if that’s your only protection and you want it to fail closed. - Want agents separated from your own account with the least setup: Option 1.
- Want each stage to run in its own disposable environment: Option 2.
- Want the entire device server — not just individual stages — walled off from the host: Option 3.
Options 2 and 3 cannot run together. Option 1 addresses a different layer (which OS account an agent runs as) than Options 2 and 3 (containment via container).
Related guides
- Managing your devices — health checks, readiness, and the Docker ready check this page’s Option 2 adds
- Running the device server as a service — where to set environment variables under systemd or launchd, and the “Docker container (host-isolated)” option this page’s Option 3 uses
- Customising task types and workflows — the Writable switch that governs push-credential exposure per stage
- Following a live agent session — watching or dropping into a session, unchanged by any of the options above
- Running a stage in your own container image — naming a custom image and setup script for a stage running under Option 2
