CodeHerderSearch⌘KRequest access →

← All docs

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 (default ch-agent).
  • --group <name> — a shared group used to grant the setup (default ch-agents).
  • --ch-path <path> — path to the ch binary 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):

  1. Log out and back in — or restart ch device-server — so your new group membership takes effect.
  2. 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, set CH_AGENT_GROUP to match.
  3. 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=0 if 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).

CodeHerder

Round up your herd.

Bring every human and every agent onto one table. Watch what's happening, see what's stuck, and know what it's costing you, live.

Try "pricing", "connect a device", or "who reviews the code"

↑↓ move · ↵ open · esc close