build block of .clawker.yaml: system packages, language stacks, typed build instructions, and raw Dockerfile injection points.
The Two-Stage Build
Each project gets a shared base image plus one harness image per coding-agent harness you build:
The base image starts from a pinned
debian:bookworm-slim digest and layers in the development floor every container gets: git, zsh, sudo, gh, curl, wget, jq, fzf, make, gcc, editors (nano, vim), locales, and the Docker CLI (for socket-mount workflows). Your build.packages install into the same apt layer.
The harness image adds whatever the selected harness declares — for example, claude installs Claude Code (plus the node stack it runs on), while codex installs the standalone Codex binary — and ends with ENTRYPOINT ["clawkerd"] and the harness’s CMD.
Caching
The base image is keyed by a content hash of its inputs (the rendered base Dockerfile plus the contents of anyinstructions.copy sources), stamped as an image label. Rebuilding a harness image reuses the existing base whenever that hash is unchanged — editing source files outside your copy sources never triggers a base rebuild. Within each stage, Docker’s normal layer cache (BuildKit or classic) skips unchanged layers. Use clawker build --no-cache to force a full rebuild.
Building and Tags
-t NAME builds that harness, and the resulting tag is the harness name. NAME is a bare name for a built-in or loose harness, or a qualified namespace.bundle.component address for a harness from an installed bundle (see Harnesses). Building the default harness also stamps the :default alias. The default is the built-in claude harness unless the build.harness key selects another (see Setting the default harness).
The @ shortcut in run/create commands resolves against these tags:
dev.clawker.harness label identifying which harness they were built for.
Build-time variables can be overridden with --build-arg on clawker build — for example --build-arg CLAUDE_CODE_VERSION=2.1.4 pins the claude harness’s install to an exact version, or --build-arg NODE_VERSION=22 pins a stack’s runtime line. When a --build-arg targets an ARG the base image declares, Clawker folds the value into the base freshness key so changing it rebuilds the base (a build arg the base doesn’t declare — harness-only or unknown — never triggers a base rebuild).
System Packages
Install additional Debian (apt) packages with thepackages field:
Stacks
Stacks are reusable language-runtime definitions. Declaring one installs it in the shared base image, before yourroot_run/user_run instructions execute — so your instructions can rely on it:
go, node, python, rust, java, ruby, cpp, and
dotnet stack definitions. Each entry is
a component name — bare for a built-in or loose stack, or a qualified
namespace.bundle.component address for a bundled one. See Stacks for
where stacks come from and how to author your own.
Stacks are self-guarding — each skips itself when the image already provides the runtime. A project-declared stack renders in the shared base before your root_run/user_run; a harness-declared stack (for example, claude declares node) renders in the harness image. Both strata render even when they share a name — the fragment self-guards handle any overlap.
Build Instructions
Theinstructions block provides type-safe build directives. All of these render in the shared base image.
env
Environment variables baked into the image:copy
Copy files from your project into the image with optionalchown/chmod:
copy steps render after user_run so expensive setup layers stay cached when a copied file changes.
labels
Add metadata labels to the image:args
Define build-time arguments (override with--build-arg at build time):
root_run and user_run
Run commands during the build.root_run executes as root, before the container user is created; user_run executes as the container user, after the user switch. Both run after any project-declared stacks, so go, node, python, etc. are available:
user_run commands execute under /bin/zsh -o pipefail -c, not /bin/sh —
clawker bakes zsh as the build shell so its shell tooling is available. Two
consequences for scripts written against POSIX sh: zsh does not word-split
unquoted variable expansions (quote your variables), and pipefail is active
(a failing command anywhere in a pipe fails the build). root_run runs earlier
in the build and is unaffected — it still uses /bin/sh.Injection Points
For advanced use cases, theinject field provides raw Dockerfile injection points. Each field accepts an array of raw Dockerfile lines. Four points render in the base image, two in the harness image:
clawker-<project>:base):
FROMthe pinned substrateafter_from— apt sources, proxy config, CA certs that package installation depends on- Package installation (Clawker’s floor + your
build.packages) after_packages— post-install configuration as root- Project-declared stacks (root scope), then your
root_run - Container user creation
after_user_setup— directories, permissions, services (still root)USERswitch to the unprivileged container user (clawker)after_user_switch— dotfiles, shell config- Project-declared stacks (user scope), then your
user_run, thencopy
clawker-<project>:<harness>, built FROM the base):
- Harness-declared stacks + the harness’s install steps (e.g. the harness CLI)
user_commands— runs as the container user, after the harness’s fragment blocks and config seeds: add MCP servers, install plugins or extensionsbefore_entrypoint— final tweaks before Clawker’s runtime assets andENTRYPOINT
user_commands and before_entrypoint live in the harness image, they run once per harness you build — but the same build.inject applies project-wide, so a step that registers an MCP server with claude runs in every harness image you build. To scope an inject step to a single harness, use a per-harness overlay (below).
Per-Harness Overlays
The basebuild block applies to every image you build. To layer extra stacks, packages, or inject steps onto one harness’s image without touching the harness definition or the base, declare them under build.harnesses.<name> — the same stacks, packages, and inject primitives, scoped to that harness’s lineage. The <name> key uses the same spelling you select the harness by: a bare name for a built-in or loose harness, or a qualified namespace.bundle.component address for a bundled one.
stacksrender in that harness’s image after the harness’s own stacks (installer → overlay, declaration order). A name that appears in both the harness and the overlay renders once, at its installer position.packagesinstall as an apt step in that harness’s image. They are not deduplicated againstbuild.packages— apt idempotence handles any overlap.injectexposes only the two harness-image points,user_commandsandbefore_entrypoint. Overlay inject renders after any globalbuild.injectat the same anchor, and only in that harness’s image — this is how you scope aclaude mcp addto the claude container instead of every harness.