-
Notifications
You must be signed in to change notification settings - Fork 0
stacks
A stack is a manifest (catalog/stacks/<name>/stack.yaml) that composes a chosen set of recipes
(and optional shared services). It is harness-free: the harness is a required positional chosen
at build/run time (harnessed <stack> <harness>), so the same stack runs on claude, omp, or any
other harness, materialized into per-harness images and pods. A running stack is a podman pod —
harness container + hatago + any declared services — composed at runtime, not baked at build time
(FROM can't union sibling systems; design §3, §6).
For the why (the runtime-pod model, and why the harness is not a stack property), read
docs/harnessed-design.md §2 & §12. This guide shows the how with worked
examples from this repo's catalog/stacks/.
catalog/stacks/<name>/stack.yaml # the manifest (you author or scaffold)
↓ harnessed build <stack> # assemble (emit-only) + scan + host podman build
$XDG_DATA_HOME/harnessed/profiles/<name>/ # GENERATED (never committed); mounted into the harness container
.claude/{skills,commands,...} # the assembled, version-controlled profile
hatago.config.json
Recipes are resolved ahead of time into a committed profile plus pinned images; the host runs
podman build, and nothing is assembled at container start (design §15).
The typed model lives in src/harnessed/schema.py (Stack).
Key fields:
name: <stack> # required
harnesses: [claude] # optional — harnesses to build when no explicit harness is given
recipes: [a, b, c] # list of catalog/recipes/ to compose
services: [ping] # optional — shared sidecars to attach (auto-started on launch)
permissions: yolo # optional — prompt | auto (default) | yolo (writes skip-permission config)
instructions: | # optional — identity text emitted to .claude/CLAUDE.md at assemble time
You are a <role> agent.
state: # optional
persist: true # default; `--fresh` overrides at runtime
session_state: host # host (default — sessions persist, inspectable) | volume
extends: <stack> # optional — inherit from a named base stack; see the extending-stacks guideNotes:
-
The harness is a run-time argument, not a stack property:
harnessed <stack> <harness>. The optionalharnesses:list is a build-time convenience —harnessed build <stack>with no explicit harness builds every harness listed there;harnessed build(no stack) includes these pairs in its reconciliation sweep. Note:harness:(singular) is rejected by the schema validator with a suggestion to useharnesses:. -
instructions:is the stack's identity text, emitted into the assembled profile at build time. For theclaudeharness it lands in.claude/CLAUDE.md(Claude Code's project instructions file); forompit is emitted as an identity block in APPEND_SYSTEM.md; forcodexit becomes~/.codex/AGENTS.md. A stack withoutinstructions:leaves the harness's built-in defaults untouched.instructions:is a single text block, not concatenated with reciperules:files (which fan into.claude/rules/separately). -
extends:names a base stack to inherit from. Union-merged fields:recipes,services,harnesses,ssh_keys(parent's entries first, then the child's, de-duped). Everything else is a child-wins override or inherited when omitted. See the extending-stacks guide for full merge semantics. -
claudemounts the profile natively;ompconsumes the same Claude-canonical profile viaclaude-hooks-bridge— no re-authoring needed for either harness. -
session_state: volumeisolates a stack's history instead of sharing it with the host — usually not what you want. The governing design principle across harnessed is to containerize the configuration (skills, MCP servers, rules), not the storage: your conversations, usage, and stats should persist to the host and stay coherent regardless of which stack launched a session, so switching between different tool configurations for the same project never fragments your history.session_state: host(the default) is what delivers that.volumedoes the opposite — it's a genuine, sometimes-useful escape hatch for a deliberately throwaway, non-continuous stack, but it's easy to pick up by accident (e.g. copied from an example) and silently split your history per-stack instead. Leave it onhostunless you specifically want isolation. - Only the fields you exercise are required; the assembler parses the rest forward.
catalog/stacks/gsd-core_repowise/stack.yaml
is the smallest general-purpose shipped example — two recipes, no services, no harness pin:
name: gsd-core_repowise
recipes: [repowise, gsd-core]
services: []The full lifecycle (harness chosen at run time):
harnessed build gsd-core_repowise claude # assemble → scan → build hatago → image scan
harnessed gsd-core_repowise claude # launch the pod (harness + hatago), attach
harnessed test gsd-core_repowise claude # capability report for this harnessharnessed build emits the $XDG_DATA_HOME/harnessed/profiles/gsd-core_repowise/claude/ tree
(assembled from catalog/recipes/repowise and catalog/recipes/gsd-core) and builds the hatago
image. harnessed gsd-core_repowise claude composes the pod and attaches; harnessed test brings
the instance up --fresh headless and asserts the manifest's declared capabilities are live (design
§18). Running an unbuilt stack errors and tells you to harnessed build first.
An illustrative ping_time stack (scaffold it with harnessed new, below) composes a stdio
recipe (time) with a service-ref recipe (ping) and attaches a shared sidecar:
name: ping_time
recipes: [time, ping]
services: [ping]The manifest names no harness — harnessed ping_time claude and harnessed ping_time omp run this
same stack. A stack whose name collides with a harness name is rejected outright, which is why this
example is ping_time and not claude_ping_time.
-
recipes: [time, ping]— the assembler composes two recipes into one profile: thetimestdio server (hatago child) and thepingnetwork-native server (hatago URL-proxy). The capability test asserts both. -
services: [ping]— the launcher auto-starts thepingsidecar on launch (ensure_service_up) if it isn't already running. The service is a standalone container (own image + volume), not a pod member; its lifecycle is independent of any instance.
Authoring the sidecar itself is covered in the service-authoring guide.
catalog/stacks/gsd-core_repowisecomposes two recipes (repowise,gsd-core) with no harness pin —harnessed gsd-core_repowise claudeandharnessed gsd-core_repowise ompboth build from the same manifest, proving that one canonical profile runs on either harness.
--extends defaults to the stack named default on both run verbs, and a --recipe set mints a
manifest that says extends: default. So the shipped
catalog/stacks/default
is the baseline behind every launch you did not write a manifest for:
name: default
recipes: [default]
services: []One recipe, no services, and deliberately no policy fields — a shipped baseline that set
permissions: or turned on credential forwarding would apply that policy to every install without
being asked for. The recipe it composes,
catalog/recipes/default,
carries the harnessed-catalog authoring skill and is kept small for the same reason: everyone pays
for what the baseline holds.
To use your own baseline, author a default stack in your overlay. The user catalog is searched
first and wins on a name clash, so ~/.config/harnessed/catalog/stacks/default/stack.yaml replaces
the shipped one wholesale — for every dynamic stack, in every project:
# ~/.config/harnessed/catalog/stacks/default/stack.yaml
name: default
permissions: auto
recipes: [default, ccstatusline, my-house-recipe]
forward_git_credentials: trueharnessed also seeds a copy of the default recipe into
~/.config/harnessed/catalog/recipes/default on first run, so the recipe in effect is one you can
edit. The overlay wins there too — which means changes to the shipped recipe in later harnessed
releases stop reaching you once you have been seeded. Delete your copy to go back to the shipped
one; it is re-seeded on the next run.
--no-extends opts out of the baseline entirely: the recipe list stands alone.
harnessed new writes a manifest for you, refusing to overwrite an existing stack:
harnessed new my-stack --recipes time,greet
# → writes catalog/stacks/my-stack/stack.yaml:
# name: my-stack
# recipes:
# - time
# - greet
# services: []There is no --harness flag: the harness is a run-time positional, not a stack property. What the
command does reject is a stack named after a harness (harnessed new claude fails), since that
name would be ambiguous on the command line.
--recipes is optional; omitting it scaffolds an empty recipes: [] you fill in by hand.
| Step | Command | Notes |
|---|---|---|
| Build | harnessed build <stack> |
Assemble (emit-only) + scoped source scan + host hatago build + image scan. Fails on HIGH. |
| Run | harnessed <stack> [path] |
Compose the pod (harness + hatago), attach. Auto-builds missing images. |
| Clean-room run | harnessed <stack> --fresh |
Tear down any existing pod/instance first; reseed state from the profile. |
| Capability test | harnessed test <stack> |
Launch --fresh headless + assert declared capabilities (markdown report). |
| List | harnessed list |
Authored stacks + running instances. |
| Stop / remove | harnessed stop | rm <stack> |
Stop or remove every instance of a stack (across projects). |
| Install | harnessed install <stack> |
Write a ~/.local/bin/<stack> launcher shim (launch by name from any cwd). |
State persists by default: an instance writes projects/ + history.jsonl to a
harnessed-owned host dir with a legible project slug (STA-02). --fresh is the throwaway path. See
the troubleshooting guide for the state-dir layout and --fresh semantics.
- docs/harnessed-design.md §2 & §12 — the why (the stack model, the stack manifest, state).
-
Extending stacks —
extends:inheritance: merge semantics, chain resolution, security constraints. - Recipe-authoring guide — author the recipes a stack composes.
- Service-authoring guide — author the sidecars a stack attaches.
-
src/harnessed/schema.py— the typedStackmodel.
Start Here
Guides
- Recipe authoring
- Service authoring
- Stacks
- Extending stacks (proposed)
- Recipe catalog
- System prompt & rules (proposed)
- Secrets
- AWS SSO
- Pulumi (host login forwarding)
- Egress & exposing services
- Container filesystem
- Git hooks
- Troubleshooting
- Pin management (harnessed update)
Codebase Map
Planning & Roadmap
- open work: GitHub Issues
Research & Prompts
- research/ (home-folder requirements per harness, browse in-repo)
- prompts/ (reusable prompt templates, browse in-repo)