Skip to main content
Clawker containers are more than a simple docker run. Every container goes through a multi-phase initialization that sets up workspace mirroring, git integration, harness state persistence, credential forwarding, and security controls. This page explains what happens under the hood and why.

Container Lifecycle

When you run clawker run @ or clawker container create @, the container goes through four host-side phases before clawkerd takes over inside. Creating a container does not require the control plane — the bootstrap material is minted against the host clock (the source of truth) and staged without contacting CP. The control-plane readiness and clock-sync gate is enforced later, on the start path (clawker container start, or the start half of clawker run): before clawkerd exchanges the baked-in assertion, the CLI ensures CP is up and waits until CP’s clock has caught up to the host (see control plane clock sync if a start fails this check). The four host-side phases are:
  1. Workspace — resolve the working directory, set up mounts, ensure volumes
  2. Config — initialize the harness’s config volumes (first run only)
  3. Environment — start the host proxy, forward git credentials, resolve environment variables
  4. Container — validate flags, build Docker configs, create the container, stream bootstrap material (mTLS leaf cert, CA cert, Hydra JWT) into the container’s writable layer, then start it
After docker start, the control plane attaches eBPF firewall programs from outside and dials into the container’s mTLS listener to drive a fifth phase inside the container — config seeding, git and credential wiring, any post_init script you’ve configured — before the readiness signal tells clawkerd to fork the user CMD (the harness). Each host-side phase streams progress events to the terminal; clawkerd renders the in-container init steps on the same attached TTY so you see one unified boot log.

Create vs Start vs Run

A fresh container initializes once: the host-side phases run at creation time, and the in-container init phase (config seeding, git wiring, your post_init script) runs the first time the container starts. Restarting a container (docker stop/start) re-runs only the boot phase — it respawns the harness and re-runs your pre_run script, but it does not re-run init. Your harness state, config, and command history survive restarts. Recreating the container (remove + create) against pre-existing volumes preserves that state too — init steps that already ran (like post_init) are skipped thanks to markers on the lifecycle volume.

Workspace Mounting

The most important mount is the workspace — your project source code made available inside the container. Clawker supports two workspace modes:

Bind Mode (default)

Your host directory is mounted directly into the container as a live bind mount. Changes on either side are visible immediately. This is the default because it gives the agent real-time access to your latest code.

Snapshot Mode

A one-time copy of your project is placed into a Docker volume. The container works on an isolated snapshot — changes inside the container don’t affect your host, and vice versa. Useful when you want the agent to experiment without risk.

Path Mirroring

Here’s something subtle but important: the workspace is not mounted at a generic path like /workspace. Instead, Clawker mirrors your host’s actual directory structure inside the container. If your project lives at /Users/schmitthub/Code/myapp on the host, it appears at /Users/schmitthub/Code/myapp inside the container too. The container’s working directory is set to match. Why? Coding-agent harnesses key state off the working directory. The concrete case is the claude harness: Claude Code tracks sessions by the current working directory, and when you use /resume to pick up a previous conversation, it discovers your project’s git worktrees and looks for session files that match those paths. If the container used a synthetic path like /workspace, those session lookups would fail — the paths wouldn’t match what git reports, and Claude Code would say “No conversations found.” By mirroring the real host path, everything lines up naturally: sessions created in the container are findable by /resume.

Git Integration

Clawker makes git work seamlessly inside containers, even for advanced setups like worktrees.

Standard Repositories

For a normal (non-worktree) project, the .git directory is part of your workspace and comes along with everything else — bind-mounted in bind mode, or copied into the volume in snapshot mode. Either way, git commands inside the container work exactly as they do on your host (snapshot’s .git is a disposable copy, so history and commits inside the container never reach the host). Exclude it from a snapshot by adding .git/ to .clawkerignore.

Worktree Support

Worktree containers mask .git/hooks and .git/config read-only as a security measure for unattended sessions. See Worktree Caveats for the behavioral consequences (git config --local, git remote add, git push -u).
Git worktrees are more complex. A worktree is a separate checkout of your repository that shares the same .git metadata as the main repo. The worktree directory contains a .git file (not a directory) that points back to the main repository’s .git/worktrees/<name>/ metadata. The challenge: those .git file references use absolute host paths. If the main repo is at /Users/schmitthub/Code/myapp, the worktree’s .git file says something like:
For git to work inside the container, that path must resolve. Clawker handles this by mounting the main repository’s .git directory at its original absolute path inside the container. The mount source and target are identical — if the .git directory lives at /Users/schmitthub/Code/myapp/.git on the host, it appears at exactly that path in the container. Combined with path mirroring for the worktree directory itself, git commands work correctly: the .git file’s reference resolves, git finds the shared metadata, and operations like git log, git status, and git commit all behave normally.

Credential Forwarding

Git credentials are forwarded into the container automatically based on your project’s security settings:
  • HTTPS — Clawker runs a host proxy that the container’s git credential helper calls through. Your host’s git credentials are never copied into the container.
  • SSH — SSH agent forwarding is handled via a socket bridge (muxrpc over docker exec). Your SSH keys stay on the host.
  • GPG — GPG agent forwarding works the same way as SSH, via the socket bridge.
  • Git config — Your ~/.gitconfig is bind-mounted read-only at /tmp/host-gitconfig inside the container. During CP-driven init, clawkerd reads that file, filters out any [credential] sections, and writes the sanitized result to ~/.gitconfig inside the container (which then uses its own credential forwarding).

State Persistence

Harness state — sessions, settings, auth tokens, command history — is preserved across container restarts and recreations through persistent Docker volumes.

Harness Config Volumes

Each harness bundle declares the directories it persists as a list of {name, path} entries in its manifest. Every declared entry becomes a named Docker volume, mounted at that path under the container home:
  • The claude bundle declares a config volume at ~/.claude — session transcripts (JSONL files under projects/<mangled-cwd>/), the .config.json state file, plugins, and settings
  • The codex bundle declares a config volume at ~/.codex — everything Codex keeps under $CODEX_HOME
Volumes are named clawker.<project>.<agent>-<harness>.<name> (e.g. clawker.myapp.dev-claude.config). The harness segment is added automatically — bundles declare only the name, and cannot opt out. It keeps each harness’s state on its own volume, so Claude’s ~/.claude can never land on the volume Codex writes to. For a bundled harness the segment is the fully qualified <namespace>.<bundle>.<component> spelling you selected it by. Because these are Docker volumes, they survive container removal and recreation. As long as you use the same project, agent, and harness, your harness state — including the auth token from your in-container login — is preserved.

History Volume

A clawker.<project>.<agent>-history volume preserves shell command history at /commandhistory, so your bash/zsh history carries over between sessions.

Lifecycle Volume

A clawker.<project>.<agent>-<harness>.clawker volume backs ~/.clawker inside the container. It is harness-scoped for the same reason the config volumes are: it carries that harness’s staged seeds and its post-init marker. It holds Clawker’s lifecycle state: delivered hook scripts (post-init.sh, pre-run.sh), the harness’s staged config seeds plus their manifest, and the post-init marker. It lives on its own volume — with the same lifetime as the config volumes that post_init mutates — so a recreated container doesn’t re-run one-time initialization against volumes that were already initialized.

First-Boot Config Seeding

Harness bundles can declare seeds — config files applied into the harness’s config volume on first boot. The image stages each seed file under ~/.clawker/seed/ along with a manifest; on the first start, the control plane dispatches a generic seed-apply step that places each file according to its apply mode:
  • copy-if-missing — copy unless the destination already exists
  • copy-if-missing-or-empty — copy unless the destination exists and is non-empty
  • json-merge — merge, with the existing file taking precedence over the seed
The claude bundle, for example, seeds a statusline script, a .config.json that marks onboarding complete (so Claude Code skips the first-run wizard), and a base settings.json. The step is harness-blind: which files seed where is data the image carries, and CP just applies the manifest.

Host Config Staging

At create time, when a config volume is newly created and the harness’s config.strategy is copy (the default), Clawker stages your host’s managed harness configuration into it. For the claude harness that means an allowlisted subset of settings.json, your global CLAUDE.md, agents, skills, commands, and plugins (with host paths rewritten to container paths); for codex it means your global AGENTS.md and prompts. Credentials are never staged — see Credential Forwarding. Pre-existing volumes carry your in-container state and are never re-seeded. Set harnesses.<name>.config.strategy: fresh to skip staging entirely. Some harness bundles additionally declare live host-state mounts — the claude bundle bind-mounts your host’s ~/.claude/projects/ over the config volume’s projects/ subdir, so sessions and auto-memory are shared live between host and containers. Disable per harness with harnesses.<name>.mount_projects: false.

How Resume Works (claude harness)

When you type /resume inside Claude Code, it needs to find previous sessions for the current project. Here’s the discovery process:
  1. Claude Code reads the .git metadata in the current working directory
  2. It discovers all git worktrees associated with the repository (the main checkout plus any worktrees)
  3. For each worktree path, it looks for session files stored under that path’s identifier
  4. It presents matching sessions for you to resume
This is why path mirroring matters. The working directory inside the container must match a real path that git worktree discovery returns. If the container used /workspace as its working directory, Claude Code would store sessions under a /workspace identifier, but the worktree discovery step would return host-absolute paths. The identifiers wouldn’t match, and resume would find nothing. With path mirroring:
  • Container cwd = /Users/schmitthub/Code/myapp (matches the host)
  • Sessions stored under the /Users/schmitthub/Code/myapp identifier
  • Git worktree discovery returns /Users/schmitthub/Code/myapp in its list
  • /resume finds the sessions
Multiple containers working on the same project (but different agents) each get their own sessions that are all discoverable via /resume.

Post-Init Scripts

If your clawker.yaml includes a post_init script (harness-agnostic under agent.post_init, per-harness under harnesses.<name>.post_init — both run, base first), it runs once during the in-container init phase — dispatched by the control plane to clawkerd as a setup command before the user CMD is forked. A marker file on the lifecycle volume prevents it from running again on subsequent restarts or recreations. To re-run, delete the marker (~/.clawker/post-initialized) or remove the lifecycle volume. This is useful for project-specific setup like installing dependencies or configuring MCP servers — anything that needs to run inside the container with agent.env available, before the harness starts.

Pre-Run Scripts

If your clawker.yaml includes a pre_run script (again composable: agent.pre_run plus harnesses.<name>.pre_run), it runs on every container start — run, start, and restart — right before the user CMD is forked, in the container workdir. Unlike post_init, there is no marker: it re-runs unconditionally each time. The CLI re-delivers the script fresh on every start, so editing or removing pre_run takes effect on the next start without recreating the container. The canonical use case is npm install when node_modules lives in a tmpfs (wiped on stop/restart) or package.json drifts upstream — anything that must be re-established each boot. A non-zero exit is fatal: the boot phase halts, the readiness signal is never sent, and the container exits non-zero (the harness never starts).

Volumes and Naming

Clawker uses a consistent naming scheme for all Docker resources: The history and workspace volumes carry no harness segment on purpose: shell history and your workspace snapshot belong to the agent, not to whichever harness is driving it, and stay shared when you switch harnesses. All resources are tagged with labels (dev.clawker.project, dev.clawker.agent) for filtering and management. Anything created under a harness’s directive — its containers, its harness image, and its harness-scoped volumes — additionally carries dev.clawker.harness, recording which harness made it. That makes docker volume inspect answer “which harness owns this?” without guessing at the name. The clawker container ls command uses these labels to show only Clawker-managed containers.

Volume Lifecycle

  • Config, history, and lifecycle volumes persist independently of containers. Removing a container does not remove its volumes. clawker volume prune sweeps all unused agent volumes by default. Use clawker volume prune --all to additionally clean up infrastructure volumes (monitoring stack and any other clawker-managed volumes). For targeted cleanup, prefer clawker volume list + clawker volume remove.
  • Workspace volumes (snapshot mode only) are ephemeral and tied to the container lifecycle.
  • Volume cleanup on failure — If container creation fails partway through, only volumes created during that attempt are cleaned up. Pre-existing volumes with your session data are never touched.

Container Image

Clawker builds per-project images tailored to your project: a shared base image plus one image per harness. The harness image a container runs includes:
  • A pinned Debian substrate plus Clawker’s baseline development tooling (git, zsh, gh, editors, Docker CLI)
  • System packages you’ve specified (build.packages) and any declared language stacks
  • The selected harness CLI, installed by its bundle (e.g. Claude Code via its native installer, Codex via its standalone installer), with the bundle’s config seeds staged for first boot
  • A baked-in copy of clawkerd at /usr/local/bin/clawkerd — the per-container daemon that runs as PID 1
  • Credential helper binaries (git-credential-clawker, socket-bridge server)
  • The unprivileged container user (clawker) with sudo access — on Linux hosts the UID is baked at build time to match the CLI invoker’s os.Getuid() so host-state bind mounts stay writable from inside the container; macOS/Windows hosts (Docker Desktop virtiofs) fall back to UID 1001
The container’s ENTRYPOINT is clawkerd; the CMD is the harness command declared by the bundle (claude, codex, …). See Image Customization for the build model and customization surface.

Agent Awareness Prompt

Clawker ships a managed agent-context prompt and bakes it into the image at build time, at the location the harness declares it reads managed context from (the harness manifest’s managed_prompt — for the claude harness, /etc/claude-code/CLAUDE.md, which Claude Code loads automatically). The agent gains awareness of its containerized environment without any user configuration; a harness that declares no managed-context location gets no prompt. The prompt tells the agent:
  • What it can do — read/write workspace files, run commands, install packages, use git (credentials forwarded from host)
  • What it cannot do — modify firewall rules, access the host filesystem outside the workspace, manage other containers
  • How the firewall works — DNS queries for unlisted domains return NXDOMAIN, connection failures mean the domain isn’t allowlisted
  • How to help the user — when the agent hits a blocked domain, it explains the problem and suggests the correct clawker firewall add, clawker firewall bypass, or clawker firewall disable command for the user to run on the host
  • Environment diagnostics — lists environment variables (CLAWKER_PROJECT, CLAWKER_AGENT, CLAWKER_WORKSPACE_MODE, CLAWKER_FIREWALL_ENABLED, etc.) the agent can inspect for troubleshooting
This creates a self-service experience: when a network connection fails, the agent diagnoses it and tells the user exactly what to do — no manual troubleshooting required.

clawkerd: The Container’s PID 1

Every agent container runs clawkerd as PID 1. clawkerd is the per-container supervisor — it owns the container’s lifecycle, terminates correctly under docker stop, signs into the clawker control plane over mTLS, and only then forks the user CMD (the harness) with kernel-side privilege drop. clawkerd itself runs as root inside the container. It does not drop its own privileges — privilege drop happens kernel-side, between fork and exec, on the child it spawns. The supervisor stays root to: write /var/log/clawker/clawkerd.log (rotated), read its mTLS bootstrap material, reap reparented orphan zombies via Wait4(-1), and hold open the mTLS listener.

Why PID 1 Matters

A PID 1 process in Linux has special responsibilities the kernel does not delegate elsewhere:
  • Signal handling. SIGTERM, SIGINT, SIGHUP are not handled by default for PID 1. A harness CLI running naively as PID 1 would ignore docker stop until the 10-second grace expired and Docker escalated to SIGKILL — leaving no time for clean teardown of the agent’s session state. clawkerd installs explicit signal handlers and forwards forwardable signals to the user CMD’s process group.
  • Zombie reaping. Any orphaned process whose parent dies is reparented to PID 1. If PID 1 doesn’t call Wait4(-1), those zombies accumulate forever. clawkerd runs a two-phase reaper: phase 1 reaps only the user CMD (so concurrent exec pipelines from CP don’t get their child stolen); phase 2 drains orphans after the user CMD has exited.
  • Exit code propagation. Docker reads PID 1’s exit code as the container’s exit code, which is what restart: on-failure switches on. clawkerd maps the user CMD’s wait status to the bash convention (WEXITSTATUS for normal exit, 128 + signum for signaled) so Docker’s restart policy interprets it correctly.

Boot Sequence

When the container starts, clawkerd executes in this order:
  1. Read bootstrap material. Four files in /run/clawker/bootstrap (per-agent mTLS leaf cert + key, the clawker CA cert, and a single-use Hydra JWT). The CLI streams these into the container as a tar archive between docker create and docker start — they live on the container’s writable layer, not on tmpfs or a bind mount.
  2. Resolve environment. CLAWKER_AGENT is required; CLAWKER_PROJECT may be empty (for global-scope containers). CLAWKER_USER (defaults to clawker) is resolved against /etc/passwd to populate the kernel-side privilege-drop credentials for the eventual spawn.
  3. Start the mTLS listener. clawkerd serves a gRPC ClawkerdService.Session endpoint on :7700, reachable only over the clawker-net network. The listener enforces RequireAndVerifyClientCert plus a CN pin: the only authorized peer is the clawker control plane’s cert (CN = clawker-controlplane). Any other peer is rejected at the TLS layer, before any RPC handler runs.
  4. Wait for CP-driven init. The control plane dials in over mTLS, registers the agent’s identity (binding the container ID to the captured cert thumbprint in CP’s sqlite agent registry), and then dispatches one or more setup commands — config seeding, git wiring, your post_init script if configured. Each step’s exit status and output flow back over the same Session stream.
  5. Fork the user CMD when the agent is ready. When CP sends the readiness signal, clawkerd forks the user CMD (the harness command from the image’s CMD) using SysProcAttr.Credential so the kernel performs setgroups → setgid → setuid between fork and exec. The user CMD never runs as root; clawkerd does, but it never executes user-controlled code in its own process.
  6. Supervise. clawkerd holds open the Session for the container’s lifetime — CP can dispatch additional setup commands, signal-forward into the child pgroup, or trigger a clean shutdown.
  7. Reap and exit. On docker stop (SIGTERM) or when the user CMD exits, clawkerd runs an ordered teardown: stop the listener, run phase-2 orphan drain, then os.Exit with the bash-convention exit code so Docker’s restart policy reads the right value.
If the user CMD exits cleanly (e.g. you exit the harness), clawkerd exits with the same code and the container stops. If you used --rm, Docker removes the container. If you didn’t, you can start it again later with clawker start -a -i --agent <agent> — the harness volumes still hold your settings, sessions, and shell history.

Privilege Model

The kernel performs the credential switch atomically between fork and exec, so there is no user-mode code path that holds root and then drops it. clawkerd’s command surface (CP-dispatched, root-capable) is the entire trust boundary. The CN-pinned mTLS listener (CP is the sole authorized caller) is what protects it. There is no per-command argv allow-list — anything that can mint a clawker-controlplane-CN cert chained to the clawker CA gets root-equivalent code execution inside the container. Bootstrap material lives only on the container’s writable layer, which dies with --rm or docker rm.

What clawkerd Does Not Do

  • No proactive outbound dial outside the one-time CP-triggered Register handshake. clawkerd is a server — CP dials in, not the other way around.
  • No heartbeat or keepalive. CP knows liveness via Docker events plus the Session stream’s TCP keepalives.
  • No init-script execution as part of clawkerd’s own logic. Setup steps (config seeding, git wiring, post_init) are dispatched by CP over the Session as setup commands — clawkerd just runs them and reports the result.
  • No firewall configuration. eBPF cgroup programs are attached to the container’s cgroup from outside by the control plane. clawkerd has no role in firewall enforcement.
  • No reconnect logic. clawkerd serves; if CP loses the connection, CP reconnects with backoff. clawkerd just waits for the next dial.

Boot Progress on the Terminal

When you attach an interactive terminal (-it), clawkerd renders per-step boot progress in plain text: an “Active” status line for the current step, then a check mark () or cross () when the step finishes. Steps run in milliseconds typically, so there’s no spinner animation — just a clean log of what CP dispatched and how it resolved. On a non-TTY (e.g. --detach), the same events go to clawkerd’s structured log at /var/log/clawker/clawkerd.log (rotated, 50MB × 3 backups, 7-day retention). After the final readiness step completes, clawkerd transfers TTY foreground to the user CMD and the harness takes over the terminal.

Firewall Decoupling

The firewall is intentionally not configured inside the container. eBPF cgroup programs are attached from outside by the control plane before the user CMD starts, and they remain attached for the container’s lifetime. This means:
  • The container image has no baked-in firewall addresses or capabilities — it is portable across different Docker network configurations.
  • Agent containers run with no Linux capabilities (cap_add: []). No NET_ADMIN, no NET_RAW. The firewall enforcement happens kernel-side, outside the container’s privilege scope.
  • A compromised user CMD cannot modify, weaken, or bypass the firewall from inside — the eBPF programs, the BPF maps, and the route table are all in kernel space owned by the host, not the container.
If you need extra capabilities for your workflow (e.g. SYS_PTRACE for debugging), add them to security.cap_add in your clawker.yaml. The firewall does not require any.

Rootless Docker

Clawker runs on a rootless Docker daemon on Linux. A stock rootless install works without configuration — clawker finds the daemon, and the full feature set is available, including the egress firewall and bind-mounted workspaces. The rootless daemon must run on the same machine as the CLI, and /etc/subuid//etc/subgid need subordinate ID ranges for your user (a standard rootless install sets these up). Clawker resolves the daemon address the same way the docker CLI does, in this order: the DOCKER_HOST environment variable, the docker.host key in clawker’s settings.yaml, the active docker CLI context (docker context show), then the conventional default socket. A rootless install records its socket in the docker CLI context, so clawker finds it there with no configuration. To pin the address explicitly:
Two capabilities need the kernel’s cooperation on a rootless daemon. For each one, clawker asks for sudo once per boot, runs a small embedded helper, and continues. Each run stages the helper to a private temporary directory, runs it once under sudo, and removes it — nothing is installed on your system, and the helpers are attested at release together with the CLI.
  • Firewall: BPF filesystem delegation. The egress firewall loads eBPF programs, and on a rootless daemon the control plane cannot mount its own BPF filesystem. The first control-plane start after each boot asks for sudo and runs a helper that mounts a delegated BPF filesystem for it; the control plane then retries and comes up normally.
  • Workspaces: ID-mapped views. A rootless daemon maps your user to container root, so a plain bind mount reaches the container’s unprivileged user root-owned and unusable. When you create or start a container in bind mode, clawker attaches an ID-mapped view of the workspace — one sudo prompt per boot — and mounts that view instead. Your files on the host keep their normal ownership. The view does not survive a reboot; the next container start re-attaches it.
In a session without a terminal, either step fails with the reason; run one interactive command after boot (for example clawker controlplane up or a normal clawker run) and headless runs work until the next reboot.

Shared Directory

When agent.enable_shared_dir is set to true, Clawker mounts a shared directory from ~/.local/share/clawker/.clawker-share/ on the host into the container at ~/.clawker-share (read-only). This lets you share files across all agents without including them in the workspace.

Security Controls

Every container runs with a deny-by-default network firewall. See Security for the full details on:
  • The selected harness’s egress floor and why you configure your own domains on top of it
  • Docker socket access
  • Linux capabilities
  • Agent awareness prompt
  • Credential isolation