Skip to content

build and images

Mike Crowe edited this page Sep 17, 2026 · 1 revision

Build and images

What harnessed build produces, in what order, and why the agent CLI installs last. This page used to be the "First-run build" section of the README.

The images

Images are built on the host with podman build the first time they are needed:

  • harnessed-base: fat toolchain image (mise, node@24, python, pnpm; no harness CLI).
  • harnessed-<harness>-<stack>: the derived stack image, built by harnessed build <stack> <harness>. FROM harnessed-base, then the stack's recipe Dockerfiles concatenated, then the agent CLI installed last, then the supply-chain scan.
  • harnessed-<agent>: FROM harnessed-base + the agent CLI (harnessed-claude, harnessed-omp, …). Not the derived image's parent. It is the fallback image a run uses for a stack with no derived image yet.

Supporting image (not part of the base → agent → stack lineage):

  • hatago: the MCP hub. It aggregates a stack's MCP servers behind one HTTP endpoint, with the light pnpm dlx / uvx servers baked in.

Assembly runs host-native in-process (no tool container). The host CLI emits the profile and the Dockerfile.harnessed-<stack>, then drives podman build.

Why the agent installs last

Agent CLI pins churn far faster than recipes. When the agent image was the derived image's FROM parent, every agent bump changed the parent's id and so invalidated every recipe layer of every stack on that harness. With the agent on top, an agent bump rebuilds only the agent layer and the scan; the expensive recipe layers stay cached.

It also makes the recipe layers harness-independent. They hang off harnessed-base with identical instructions, so a stack declaring harnesses: [claude, omp] builds its recipe layers once and both harnesses share them.

A recipe that branches on ${HARNESS} in a RUN necessarily splits that cache from its own layer onward, so keep such recipes late in a stack's recipes: list.

The build command

harnessed build                    # rebuild the shared images, then reconcile every stale stack
harnessed build -j1                # ... one stack at a time (default: half the cores, capped at 4)
harnessed build <stack>            # build every harness in the stack's `harnesses:` list
harnessed build <stack> <harness>  # assemble one stack for a harness: emit profile + build images (+ supply-chain scan)

A bare harnessed build rebuilds the shared images once, then builds every stale stack concurrently (--jobs / -j). Each build's output is prefixed with its own coloured stack(harness) tag so the interleaved podman logs stay readable, and one stack failing does not cancel the others. The failures are reported together at the end.

A stack may declare which harnesses it is built for:

name: my-stack
recipes: [superpowers, serena]
harnesses: [claude, omp]   # build-time only — the harness is still a run-time argument

harnessed build <stack> then fans out to every name in that list, and bare harnessed build includes those <stack> <harness> pairs in its sweep, so a freshly authored stack is provisioned without naming it. A stack that declares no harnesses: is unchanged: build <stack> still requires an explicit harness, and a bare build only reconciles it once it has been built at least once.

harnessed build <stack> <harness> rebuilds the base (so base-image changes propagate), assembles in-process, then builds the hatago, agent, and derived harnessed-<harness>-<stack> images. The derived image's final layer runs an in-image, advisory supply-chain scan over what actually landed. The profile is emitted to $XDG_DATA_HOME/harnessed/profiles/<stack>/<harness>/ (the clone stays immutable source) plus an advisory scan-report.json alongside it. Expect first-run latency; later runs are cache hits.

What this buys you in practice

  • A/B two memory systems. Run claude+hindsight and claude+openbrain as separate stacks side by side; neither touches your host config or the other's state.
  • Compare harnesses on equal footing. Point claude+hindsight and omp+hindsight at the same service-scoped memory volume and judge which harness drives it better. Same data, different engine.
  • Clean-room a flaky plugin. harnessed container-run <harness> --stack <name> --fresh reproduces from zero state, then tears down leaving no residue in ~.
  • Proof it built right. harnessed test <stack> <harness> brings the instance up headless and asserts it exposes exactly the MCP servers, skills, and commands its manifest declares, rendered as a per-capability markdown report.

See also

Clone this wiki locally