# Self-hosted device isolation

Source: https://codeherder.com/docs/self-host-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](https://codeherder.com/docs/agent-isolation/) 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_EXECUTION` admits 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`, set `CH_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:

1. Run `ch device setup-agent-user | sudo sh`.
2. Set `CH_AGENT_USER` to the new user.
3. 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](https://codeherder.com/docs/agent-isolation/) 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](https://codeherder.com/docs/device-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](https://codeherder.com/docs/self-hosting/#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

- [Isolating agent runs on a device](https://codeherder.com/docs/agent-isolation/)
- [Devices](https://codeherder.com/docs/devices/)
- [Self-hosting CodeHerder](https://codeherder.com/docs/self-hosting/)
