Self-hosted device isolation
What each execution mode isolates, how to isolate a shared device, and what workspaces on one device share.
An agent runs on a device you own. The execution mode of the device decides what the agent can reach. This page states the boundary of each mode. It also states what workspaces on one device share.
Defaults and your choice
Process mode is the default. CH_REQUIRE_ISOLATED_EXECUTION and CH_TRUSTED_EXECUTION are both off by default. CodeHerder ships no deployment value for them. You choose the execution mode and the strict setting. An upgrade does not change them.
Pick an execution mode
The device server runs in one of three modes. Check which modes your organization runs.
| Mode | Host boundary | Session and workspace boundary | What an agent can read | Residual risk |
|---|---|---|---|---|
| Process, on a laptop | None. The harness runs as the device user. | None. All sessions share one OS user and one session root. | Everything the device user can read, unless you set CH_AGENT_USER. |
A prompt-injected agent reaches the whole user account. Your organization must accept this risk in writing. |
--docker, on a laptop (trusted mode; consent with CH_TRUSTED_EXECUTION=1) |
The container, against a well-behaved agent only. The container runs --privileged and the agent has passwordless sudo. |
None inside the container. | The container volumes, the session bearer, the git-token pool and the AI credential pool. | A hostile agent can reach the host. |
--docker, on a cloud VM (trusted mode; consent with CH_TRUSTED_EXECUTION=1) |
The same container. A deployment may set CH_REQUIRE_ISOLATED_EXECUTION=1. That setting refuses every task stage on this mode unless CH_TRUSTED_EXECUTION is set. Set IMDSv2 hop limit 1 and drop metadata traffic from container networks yourself. |
None inside the container. | The same as on a laptop. | A privileged agent with sudo can still reach the instance role. |
The --docker-executor mode runs each stage in its own container. See Isolating agent runs on a device for that option.
What each choice satisfies
The first column is the repository credential gate. The second column is the strict setting CH_REQUIRE_ISOLATED_EXECUTION=1, for a spawn with harness approvals bypassed.
| Choice | Repository credentials | Unattended spawns under the strict setting |
|---|---|---|
| Process | Refused | Refused, unless CH_TRUSTED_EXECUTION=1 |
Process with CH_AGENT_USER |
Allowed | Refused, unless CH_TRUSTED_EXECUTION=1. All sessions share one agent user. |
--docker |
Refused | Refused, unless CH_TRUSTED_EXECUTION=1 |
--docker with CH_AGENT_USER |
Refused. The agent user has no effect here. | Refused, unless CH_TRUSTED_EXECUTION=1 |
--docker-executor, egress on, non-root image |
Allowed | Allowed |
--docker-executor with --stage-egress=false or CH_STAGE_ALLOW_ROOT_IMAGE=1 |
Refused | Refused, unless CH_TRUSTED_EXECUTION=1 |
| microVM image | Allowed | Allowed |
CH_ALLOW_UNISOLATED_GIT_CREDENTIALS=1 waives the repository gate. CH_TRUSTED_EXECUTION=1 never serves git credentials. A --docker device does not start without CH_TRUSTED_EXECUTION=1, so the strict setting admits every running --docker device.
The engineering table, with code citations, is in docs/security.md under “Execution-mode boundaries”.
See each device’s mode
Each device reports an Execution mode check. Run ch device show <device> or open the Health snapshot of the device. The summary starts with mode process, mode docker or mode docker-executor. The check never blocks work. It warns in two cases. Both apply only when the device sets CH_REQUIRE_ISOLATED_EXECUTION:
- The mode has no hostile-agent boundary and the device does not set
CH_TRUSTED_EXECUTION. The device refuses every stage that bypasses harness approvals. This covers process mode and an agent user alone. - Only
CH_TRUSTED_EXECUTIONadmits the spawns. The device runs the stages, but no boundary separates the agents.
Repository work needs agent isolation
A process-mode device warns on its Agent isolation check. It refuses a spawn for a sandbox with listed repos (agent_isolation_required). To fix this, use one of these:
- Run
ch device setup-agent-user | sudo sh, setCH_AGENT_USER, and restart the device server. - Start the device server with
--docker-executor.
The gate covers only a sandbox with listed repos. Repo-less work runs on an unisolated device, and the helper serves it no git credential.
An agent user satisfies this gate. It is not hostile-agent confinement. All sessions share one agent user and the ch-agents group.
You can accept the risk with CH_ALLOW_UNISOLATED_GIT_CREDENTIALS=1. The agent then reads the git-token pool. The check stays at warn.
Isolate a shared process-mode device
Set CH_AGENT_USER to run each agent as a separate OS user. The steps:
- Run
ch device setup-agent-user | sudo sh. - Set
CH_AGENT_USERto the new user. - Restart the device server.
The setting closes the operator’s home directory, the credential pools, and the credential commands on the device-local socket. It leaves two things open. All worktrees share one ch-agents group. The agent keeps the bearer of its own session. See Isolating agent runs on a device for the full steps.
Read-only stages on a laptop
CH_ALLOW_UNISOLATED_PLANNING stays on. A read-only stage on a laptop with no agent user can push with the developer’s own key. The stage removes only its environment. It does not hide ~/.ssh. This risk needs your organization’s written acceptance.
To refuse these stages instead, set CH_ALLOW_UNISOLATED_PLANNING=0 or set CH_AGENT_USER. A workspace admin can change CH_ALLOW_UNISOLATED_PLANNING in the device’s Settings section. See Changing a device’s settings.
The AI bearer
Every agent holds the bearer of its own session. This is needed to work.
In process mode with no agent user, and in --docker mode, an agent can also read every pooled AI credential on the device. That includes the subscription bearer, and credentials the owner added for another workspace. With CH_AGENT_USER or --docker-executor, the agent cannot read the pool. The tests TestAgentUser_CannotReadPooledCredentials and TestAgentUser_RefusedByCredentialVerbs pin this for the agent user.
One device, several workspaces
The device owner decides whether a device serves more than one workspace. CodeHerder does not decide it. To keep workspaces apart, run one device for each workspace. Or use CH_AGENT_USER and link a single workspace.
What a shared device shares
A device with several workspaces shares these components:
| Component | Shared between workspaces? | What another workspace’s session can reach |
|---|---|---|
OS user and home directory (~/.claude, ~/.ssh) |
Yes | In process mode and --docker, a session reads everything the device user reads. That includes other workspaces’ sandboxes. By default, the AWS, Docker, kube, gh and glab config variables point at empty session paths (internal/agentpty/isolated_config_env.go#isolatedConfigEnv). An agent can still read the files by absolute path. |
| Sandboxes and worktrees | One session root for all (--session-root) |
With CH_AGENT_USER, every worktree gets the one ch-agents group, so an agent can still reach another workspace’s worktree. Only --docker-executor mounts just the task’s own worktree. |
| Git mirrors | No | Each workspace and repository URL has its own mirror. In process mode, the device user can still read them on disk. |
| Credential pools | Yes | One pool for each harness on the device. See the list below. |
Device token and CH_SECRET_* values |
One token for each device | The session environment never carries the device token. The device-server moves its own token and CH_SECRET_* values out of its environment block at start (internal/envseal/envseal_unix.go#SealSelf), so an agent cannot read them from /proc/<pid>/environ. |
| Device-local socket | Yes | CH_AGENT_USER refuses the agent on credential commands. |
| Cost and usage collectors | Yes | One sweeper for each device scans the device’s session logs. It attributes each session by its marker file (workspace, task, agent). A session with no marker counts as a human session. |
| Docker socket proxy | Yes | One proxy for each launch, with one per-launch token and one owner. It does not split by workspace. Stage containers cannot reach it. See Protect the Docker socket proxy. |
Tool cache (/opt/ch-tools) |
No | Used by --docker-executor only. Each task worktree has its own volume. |
| Docker daemon and images | Yes | The whole device. |
The credential pool works like this:
- A session in workspace B can use a credential that the device owner added for workspace A. Its spend lands in workspace B.
- The server keeps no list of which credentials exist on which device.
Related guides
Last updated