# Container Isolation (Level 4)

Each agent runs in its own container with **only** the worktree mounted at `/workspace`.  The parent project doesn't exist in the container's filesystem namespace at all — `open("/Users/.../parent_project/...", "w")` fails with `ENOENT` because that path isn't reachable, not just denied.

This is the strongest isolation claw-forge offers.  It's **opt-in** because it requires a container runtime, a user-built image, and ~1–3s per-task startup latency.

## When to use this

| Want | Use |
|---|---|
| Default single-machine dev loop | Layers 1–3 + v0.8.18 OS sandbox (no opt-in) |
| Security-sensitive deployment | Container isolation |
| Untrusted agent prompts (e.g., crowd-sourced specs) | Container isolation |
| Multiple isolated tenants on a shared host | Container isolation |
| CI / automated pipelines | Container isolation |

If your dispatcher runs locally on your own dev machine with prompts you wrote, the v0.8.18 OS sandbox is sufficient.  Container isolation pays its overhead when the trust model includes "I don't fully trust the agent's behaviour."

## Setup

### 1. Install a container runtime

Either Docker or Podman works.  claw-forge auto-detects (preferring Docker if both are installed).

| Runtime | Install |
|---|---|
| Docker | <https://docs.docker.com/get-docker/> |
| Podman | <https://podman.io/getting-started/installation> |

Verify: `docker --version` (or `podman --version`) should print a version.

### 2. Scaffold and run (v0.8.34+ auto-build flow)

```bash
claw-forge init --container   # writes .claw-forge/agent.Dockerfile + flips yaml
claw-forge run                # auto-builds image on first dispatch
```

`init --container` materialises the default Dockerfile at `.claw-forge/agent.Dockerfile` and sets `agent.isolation: container` in the generated `claw-forge.yaml`. You don't set `agent.container.image` — the dispatcher hashes the Dockerfile content, derives a tag `claw-forge-agent:<sha256[:12]>`, and runs `docker build` once per content hash. Subsequent runs reuse the cached image; editing the Dockerfile produces a new hash and triggers a transparent rebuild.

For a custom pre-built image (private registry, custom build args, etc.), set `agent.container.image: my-image` in `claw-forge.yaml` — that skips the auto-build path entirely.

### The default Dockerfile

`init --container` writes this to `.claw-forge/agent.Dockerfile`. Edit it in place for non-Node stacks; the next `claw-forge run` picks up changes via content-hash invalidation.

```dockerfile
# claw-forge agent Dockerfile — generated by `claw-forge init --container`.
# Edit this file to customise the agent's runtime environment; the dispatcher
# detects content changes via sha256 and rebuilds automatically on next run.

FROM node:20-slim

RUN apt-get update && apt-get install -y --no-install-recommends \
        git ca-certificates curl \
    && rm -rf /var/lib/apt/lists/*

RUN npm install -g @anthropic-ai/claude-code

RUN useradd -m agent && \
    mkdir -p /workspace && \
    chown -R agent:agent /workspace
USER agent
WORKDIR /workspace

# No ENTRYPOINT — claw-forge's wrapper invokes `claude "$@"`.
```

### Manual workflow (for custom images / pre-built artifacts)

If you want full control — a custom base, registry-pulled image, or build args the auto-build can't express — skip the `init --container` flag and configure manually:

```yaml
agent:
  isolation: container
  container:
    runtime: auto            # docker | podman | auto
    image: my-claw-forge-agent   # required for the manual path
    pull_if_missing: false   # opt-in to auto-pull from registry
    extra_mounts: []
    extra_env: []
    network: none            # egress control (see below); omit for default bridge
```

Build the image yourself (`docker build -t my-claw-forge-agent .`) before running `claw-forge run`. The dispatcher fails loudly if the named image isn't locally available and `pull_if_missing` is false.

### Network egress control (`container.network`)

This is the one isolation dimension the OS sandbox can't cover — a compromised
dependency can still phone home over HTTPS. In container mode you constrain it
with `agent.container.network`, which maps directly to `docker/podman run
--network <value>`:

| Value | Effect |
|-------|--------|
| _unset_ (default) | Docker's default bridge — claude can reach the Anthropic API. Backward compatible. |
| `none` | No network at all. Use for offline work, or point the agent at a local egress proxy via `extra_env` (`HTTPS_PROXY`). |
| `<network-name>` | Attach to a docker network **you** created with egress firewall rules — e.g. `docker network create egress-allow` plus iptables/nftables rules limiting destinations to an allowlist (api.anthropic.com, your package registry). |

`none` will break claude's API calls unless a reachable proxy is provided, so
the allowlist-network pattern is the usual choice for "locked down but still
functional."

### What happens inside

For each task, the dispatcher generates a wrapper script in `<worktree>/.claw-forge/container/`, and the SDK invokes the wrapper instead of the bare `claude` binary. Inside the container, the agent sees `/workspace` as its project root with no observable path to anything else.

## Configuration reference

### `agent.isolation`

| Value | Meaning |
|---|---|
| `process` *(default)* | Use the OS-level sandbox (v0.8.18: `sandbox-exec` on macOS, behavioural-only on other OSes for now). |
| `container` | Use container isolation.  Requires `agent.container.image` and a runtime. |

### `agent.container.runtime`

| Value | Meaning |
|---|---|
| `auto` *(default)* | Try docker first, fall through to podman. |
| `docker` | Require docker; fail if not available. |
| `podman` | Require podman; fail if not available. |

### `agent.container.image` *(required when `isolation: container`)*

The image reference the wrapper passes to `<runtime> run`.  Must be locally available unless `pull_if_missing: true`.

### `agent.container.extra_mounts`

List of additional `-v` mount specs (raw strings).  For project resources the agent needs but shouldn't receive via env-var pass-through — e.g., shared test fixtures, language toolchain caches:

```yaml
agent:
  container:
    extra_mounts:
      - "/Users/me/.cargo:/home/agent/.cargo:ro"
      - "/Users/me/test-fixtures:/fixtures:ro"
```

### `agent.container.extra_env`

List of additional env-var names to forward beyond the auth set (`ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_BASE_URL`).  Each becomes `-e <NAME>` in the wrapper, so the container reads the host's current value at run time:

```yaml
agent:
  container:
    extra_env:
      - MY_PROJECT_TOKEN
      - DATABASE_URL_TEST
```

### `agent.container.pull_if_missing`

When `true`, the dispatcher attempts a single `<runtime> pull <image>` if the image isn't locally available.  Default `false` so registry traffic is opt-in (the operator decides what gets pulled).

## What the wrapper actually does

For each task, the dispatcher generates `<worktree>/.claw-forge/container/claude_containered`:

```bash
#!/bin/bash
exec docker run --rm -i \
    -v "<canonical worktree path>":/workspace \
    --workdir /workspace \
    -e ANTHROPIC_API_KEY -e ANTHROPIC_AUTH_TOKEN -e ANTHROPIC_BASE_URL \
    [extra mounts] \
    [extra env flags] \
    my-claw-forge-agent \
    claude "$@"
```

`ClaudeAgentOptions.cli_path` points at this wrapper; the SDK launches the wrapper instead of `claude` directly.  Stdio is proxied through `docker run -i` to the container's `claude` process via inherited fds; signals are forwarded by the docker CLI to PID 1 inside the container.

## Trade-offs vs. v0.8.18 process sandbox

| Dimension | Process sandbox (v0.8.18) | Container (v0.8.19) |
|---|---|---|
| Filesystem namespace | Parent project visible (read), denied for write | Parent project absent entirely |
| Setup | Zero — works out of the box on macOS | Install runtime + build image |
| Per-task startup | ~10ms (fork/exec sandbox-exec) | 1–3s (container spawn) |
| Cross-platform | macOS only (Linux planned) | macOS + Linux + Windows (wherever Docker runs) |
| Network isolation | None (host network) | Default bridge; can be tightened via Docker config |
| Resource isolation | None | Inherited cgroup limits if configured |

**Use process sandbox when**: dev loop on a trusted local machine; prompts you wrote yourself; you need fast iteration.

**Use container isolation when**: agent prompts come from less-trusted sources; multi-tenant deployment; CI pipelines; you want to fully eliminate the parent project from the agent's universe.

## Troubleshooting

### `docker: image not found`

The image reference in `agent.container.image` doesn't exist locally.  Either build it (`docker build -t <name> .`) or set `pull_if_missing: true` to pull from a registry.

### `docker: permission denied` on Linux

Your user isn't in the `docker` group.  Either add yourself (`sudo usermod -aG docker $USER` and re-login) or switch to Podman, which is rootless by default.

### Agent can read but can't write

Inside-container permission issue.  The Dockerfile template creates an `agent` user that owns `/workspace`.  If you customise to use root or a different user, ensure that user can write to `/workspace` after the bind-mount.

### `Connection refused` calling Anthropic API from inside container

Network or auth problem.  Verify the auth env vars are set in the host shell that runs `claw-forge run` — they pass through via `-e VAR` (forward by name, container reads host's current value).  Also verify Docker's network bridge isn't blocked (default works for outbound HTTPS to Anthropic).

### Tests can't find Python/Node/Go inside the container

The Dockerfile template uses `node:20-slim`.  If your project's tests need Python or other languages, customise the base image:

```dockerfile
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y \
    python3.12 python3-pip nodejs npm git curl ca-certificates \
    && rm -rf /var/lib/apt/lists/*
RUN npm install -g @anthropic-ai/claude-code
# ... rest of template
```

## What this doesn't isolate

- **Network**: container has full outbound network by default.  An agent could exfiltrate code to an external service if so inclined.  Tighten via Docker network policies (out of scope for v0.8.19).
- **CPU / memory**: no resource limits applied.  An agent could spin a runaway process.  Tighten via Docker `--cpus` / `--memory` flags (operator can extend the wrapper or use a Compose / Kubernetes layer).
- **Daemon trust**: the dispatcher (running on the host) trusts the container runtime.  An adversarial container image could still affect the host through a runtime vulnerability.  Use trusted base images.

These are container-isolation limits in general, not claw-forge limits.  For threat models that include them, an additional layer (gVisor, Firecracker microVMs) on top of Docker is the next step.
