harnessed β Composable harness stacks (Claude Code / omp / opencode / antigravity / codex + an MCP hub + optional shared services)
Warning
Container runtimes β all WIP:
| Runtime | Status | Notes |
|---|---|---|
| podman (rootless) | π§ͺ in testing | The reference runtime (pods). Most complete path β verify your host with HARNESSED_PODMAN=1 uv run pytest tests/test_recipes_integration.py. |
| Docker | β³ pending | A shared-network-namespace path exists (--network container:) but is not yet verified. Egress firewall needs rootless NET_ADMIN (best-effort β --no-firewall to skip). Shared service sidecars are not wired (no host.containers.internal). |
Apple container |
β³ pending | Tracked follow-up. One VM/IP per container, no shared netns β needs a different networking story. |
Runtime differences (pod vs shared-netns) are handled inside the host CLI (src/harnessed/launcher.py). See troubleshooting.
You can read my announcement here
Forked from kevinMEH/code-container and extended for rootless Podman, hardware authentication (YubiKey, 1Password), Claude Code auth, composable harness stacks, and alternative AI providers.
harnessed is one executable that launches composable harness stacks β each a
podman pod running an AI coding harness (claude or omp today) plus an MCP hub (hatago) plus optional
shared services (hindsight, openbrain, β¦). You compose a named stack (one harness + chosen recipes)
and launch an authenticated instance that exposes exactly the skills/commands/MCP/
services it declares β nothing from the host config β reproducibly, with podman as the only host
dependency.
It is for developers who want to compose and trial harness configurations β different
skill/plugin/MCP/memory combinations β in clean, reproducible, throwaway-or-persistent environments
without dragging every host default into the container or polluting ~.
The full architecture and design rationale live in docs/harnessed-design.md (the why). This README is the how: install, build, and run.
Every stack runs in isolated mode: auth seeded from host credentials, config layer (skills/commands/hooks/MCP) sourced exclusively from an assembled stack profile β nothing from host config leaks in. The harness container + hatago MCP hub run as a podman pod. See the design rationale.
harnessed is a host Python CLI (Python β₯ 3.12) that drives podman directly β there is no tool
container. You need two host dependencies: podman (rootless; the reference runtime β Docker
support is pending, see the runtime table above) and uv (or pipx)
to install the CLI.
Recommended β the checked-in installer. install.sh detects podman + uv (it never
installs podman for you β that is privileged and distro-specific), then installs the CLI from a
pinned git tag:
git clone https://github.com/drmikecrowe/harnessed.git
cd harnessed && ./install.sh # --install-uv to also install uv; --uninstall to removeNo-clone path β download, review, then run (the script is auditable; read it before you trust it):
curl -fsSL https://raw.githubusercontent.com/drmikecrowe/harnessed/v0.1.0/install.sh -o install.sh
less install.sh && sh install.sh
# fast-and-loose one-liner (pinned to a tag, never a branch β we recommend reading it first):
# curl -fsSL https://raw.githubusercontent.com/drmikecrowe/harnessed/v0.1.0/install.sh | shThe
v0.1.0tag above is a placeholder β no release tag has been cut yet. Until one exists, use the manual step below or runinstall.shagainst a local checkout.
Manual (what the installer automates):
git clone https://github.com/drmikecrowe/harnessed.git
uv tool install ./harnessed # or: pipx install ./harnessedThis puts harnessed in ~/.local/bin. Make sure it is on your PATH; uv tool update-shell sets it up.
To uninstall: ./install.sh --uninstall (or uv tool uninstall harnessed / pipx uninstall harnessed).
Your ~/.config/harnessed and $XDG_DATA_HOME/harnessed data are preserved.
Working on the CLI itself? Use an editable dev env instead β see CONTRIBUTING.md (
uv sync --extra dev+export PATH="$PWD/.venv/bin:$PATH").Linux β tested on Manjaro. It should work on any systemd distro, but this is not tested. macOS/WSL: untested.
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 byharnessed build <stack> <harness>β FROMharnessed-base, then the stack's recipe Dockerfiles concatenated, then the agent CLI installed last, then the supply-chain scan.harnessed-<agent>: FROMharnessed-base+ the agent CLI (harnessed-claude,harnessed-omp, β¦). No longer the derived image's parent β it is the fallback image a run uses for a stack with no derived image yet.
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.
Supporting image (not part of the baseβagentβstack lineage):
hatagoβ the MCP hub (aggregates a stack's MCP servers behind one HTTP endpoint; lightpnpm dlx/uvxservers 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.
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 argumentharnessed 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.
Bare harnessed build rebuilds the shared base/agent/hatago images (and reconciles every declared
or already-built <stack> <harness> pair). 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 β emitting the profile 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 (images build via host podman build);
later runs are cache hits.
Build and launch the time sample stack β the time recipe (one light stdio MCP server + one standalone skill) β on the claude harness:
cd /path/to/project
harnessed build time claude && harnessed time claudetime is the smallest end-to-end stack slice: the time recipe (one light stdio MCP server + one
standalone skill), composed into a profile and run as a pod (agent + hatago). The harness is chosen
at run time (harnessed time omp runs the same stack on omp). Running an unbuilt stack errors and
tells you to harnessed build it first.
After building, verify the stack's declared capabilities with the capability test:
harnessed test time claudeharnessed test launches the stack headless, runs the two-oracle capability check, and writes a per-capability report to $XDG_DATA_HOME/harnessed/profiles/time/claude/capability-report.md (β connected / β missing).
| Command | What it does |
|---|---|
harnessed container-run <harness> [path] [--stack <name> | --recipe <name>β¦] [--fresh] |
Isolated stack on a harness: assembled profile + pod (harness + hatago). --stack and --recipe (repeatable) are mutually exclusive; with neither, runs the default baseline |
harnessed host-run claude [path] [--stack <name> | --recipe <name>β¦] |
Same stack, host-native β no podman, no container; config isolated per stack, credentials from host |
harnessed build [<stack> [<harness>]] |
Build the base/harness/hatago images (+ reconcile declared/built pairs), or assemble + build a stack β for one harness, or for every harness in its harnesses: list |
harnessed test <stack> <harness> |
Capability test: launch --fresh headless + assert declared capabilities (markdown report) |
harnessed svc up | down | recreate | sync | migrate <service> |
Manage service sidecars. recreate tears down and rebuilds β mounts and env are fixed at create time, so podman restart cannot pick up a change to how the container is built |
harnessed list |
List authored stacks (with which harnesses are built) + running instances |
harnessed stop | rm <stack> [<harness>] |
Stop / remove instances of a stack (optionally one harness) |
harnessed new <stack> [--recipes a,b,c] |
Scaffold a harness-free stack manifest |
harnessed install | uninstall <stack> |
Write / remove a ~/.local/bin/<stack> launcher shim (forwards the harness arg) |
harnessed rescan |
Re-scan installed harnessed images online (the nightly timer's trigger) |
harnessed update [--check] [--yes] [--minimum-release-age MIN] |
Find outdated catalog pins and offer to bump them. tools: entries resolve against npm / PyPI / GitHub releases / mise; pins inside install scripts and Dockerfiles are reported as unresolved rather than skipped. Pins marked hold are never bumped. Modelled on pnpm's minimumReleaseAge (same unit β minutes, default 10080 = 7 days): a release younger than the window is not offered, and as in pnpm the newest version that is old enough is offered instead, naming the newer one it passed over. --check is the CI mode: non-zero exit on a stale pin, writes nothing |
harnessed --fresh ... |
Tear down any existing pod/instance first (isolated) |
harnessed --no-firewall ... |
Skip the egress firewall for this run |
harnessed -h | --help |
Show help |
Run harnessed --help for the full surface. Scanner tokens (e.g. SNYK_TOKEN) are read from the
environment β there is no harnessed auth command (see Supply chain & security).
- Recipe authoring β writing
recipes/<name>/recipe.yaml(MCP layer + skills/commands), with worked examples. - Stacks β composing recipes into
stacks/<name>/stack.yaml, scaffolding, and the build/run/test lifecycle. - Service authoring β adding a shared sidecar under
services/(image + manifest + server). - Secrets setup β opt-in varlock + 1Password (env-only, never baked).
- AWS SSO β opt-in AWS credentials via the aws-sso ECS server (short-lived STS, env-only, never baked).
- Egress & exposing services β the container egress firewall; recipe-declared
egress:hosts + pinnedtools:to conditionally open a service (e.g. Pulumi Cloud). - Troubleshooting β podman setup, first-run build,
--fresh, host-persisted sessions, the nightly re-scan timer. - Architecture & design β the why behind every decision.
- Roadmap β where harnessed is going, at the epic level: backends, secrets, harness parity, authoring, verification.
Catalog only β which third-party recipes are shipped or wanted. For where the product is going, see ROADMAP.md.
The shipped recipes today are mostly tracer/development recipes β minimal slices used to exercise
the assembly pipeline and capability test (greet, ping, time, floating-recipe). The
non-development recipes β real third-party tooling β are the ones worth tracking:
Shipped
- gstack β Garry Tan's skill suite (browser automation,
design, PDF, β¦), installed via its upstream
./setup(catalog/recipes/gstack/). The first real non-development recipe.
Planned β packages classified in docs/todos/2026-06-27-recipe-stress-test.md (repos, install commands, data models, and architecture gaps each one surfaces):
- serena β semantic code intelligence MCP (LSP-backed retrieval/editing, 40+ languages) Β· MCP recipe
- agentmemory β persistent memory server (53 MCP tools, 12 hooks, HTTP :3111) Β· service + recipe
- headroom β context/tool-output compression before it reaches the LLM Β· MCP recipe
- gbrain β knowledge brain (synthesis, graph traversal, gap analysis) Β· service + recipe
- solidspec β multi-methodology spec-driven development CLI Β· skills recipe
- codebase-memory-mcp β codebase knowledge graph (158 languages, C binary) Β· MCP recipe
- context-mode β context-window optimization / tool-output sandbox (6 hooks) Β· MCP + hooks recipe
- tokensave β pre-indexed semantic code knowledge graph (80+ MCP tools, Rust) Β· MCP recipe
- caveman β concise-output / token-compression skill Β· skills recipe
- hindsight β memory/recall sidecar (multi-container Postgres stack) Β· existing service
- Superpowers β composable software-development methodology skill suite (TDD, code review, subagent-driven dev) Β· skills recipe
- rtk β Rust Token Killer: CLI proxy that compresses dev-command output to cut LLM tokens 60β90% Β· CLI + hooks recipe
- OB1 / Open Brain β personal knowledge infrastructure: shared persistent memory + vector search across AI tools (MCP + Supabase/Postgres backend) Β· service + recipe
- pnpm everywhere β every JavaScript install (global, per-recipe, hatago's bundled servers) uses pnpm, never
npm/npx.pnpm dlxreplacesnpx. A managed supply-chain config appliesminimumReleaseAgecooldowns and lifecycle-script default-deny. Recipe validation flags rawnpm/npxand points at the pnpm equivalent (design rationale). - In-image supply-chain scan (advisory) β the derived image's final layer runs snyk (over mise node globals + recipe installs, via a synthesized manifest; token-gated by a build secret, warn-skips without one), plus credential-free osv-scanner (recipe lockfiles) and pip-audit (the Python env). It reports a compact severity summary and writes
scan-report.jsonβ it does not fail the build. Rationale: harnessed installs third-party agent tooling whose dependency trees always carry open advisories. A hard gate blocks every build on code you do not control. Visibility is the deliverable (design rationale). - Opt-in secrets β varlock + 1Password resolve
op://refs as env only (never a profile, image layer, or repo file) β into the pod forcontainer-run, into the agent process forhost-run. Copy.env.schema.exampleto~/.config/harnessed/.env.schemato turn it on. See docs/guides/secrets.md. - Nightly re-scan β a systemd user timer re-runs osv-scanner online against installed images so a CVE disclosed after build still surfaces. See troubleshooting for setup (including the
loginctl enable-lingerprerequisite). - Secrets/auth referenced, never baked β Claude OAuth, scanner tokens, and 1Password secrets reach the instance as env or read-only mounts; never an image layer.
All examples in this repo use placeholder values only (
op(op://Private/Snyk/credential), dummy tokens) β never real credentials.
- A/B two memory systems. Run
claude+hindsightandclaude+openbrainas separate stacks side by side; neither touches your host config or the other's state. - Compare harnesses on equal footing. Point
claude+hindsightandomp+hindsightat 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> --freshreproduces from zero state, then tears down leaving no residue in~. - Proof it built right. Each stack ships a capability test: bring the instance up headless and assert it exposes exactly the MCP servers/skills/commands its manifest declares β rendered as a per-capability markdown report (β connected / β missing).
Three projects solve adjacent problems β pick the one that matches your threat model and workflow:
| This project | NVIDIA OpenShell | Anthropic devcontainer | Trail of Bits | |
|---|---|---|---|---|
| Primary use case | Power-user daily driver across multiple AI harnesses | Enterprise sandboxing with policy enforcement | VS Code team dev environments | Security auditing of untrusted code |
| Auth model | Seamless β host credentials shared into container | Credential providers inject keys; never exposed in sandbox | Per-container setup | Fully isolated |
| Threat model | Contain the AI, not the repo | Full defense-in-depth (filesystem, network, process, inference) | Consistent team environments | Malicious repos / adversarial input |
| Runtime | Podman (rootless); Docker pending | K3s (Kubernetes) inside Docker | Docker / Dev Containers spec | Docker |
| AI harnesses | Claude, omp (via bridge); more planned | Claude, OpenCode, Codex, Copilot | Claude | Claude |
Use this project if you want composable experimentation across skill/MCP/memory combinations, without the friction of re-authentication or tool switching every session.
Use NVIDIA OpenShell if you need enterprise-grade sandboxing with declarative security policies, a privacy-aware LLM proxy, and Kubernetes orchestration for multi-agent environments.
Use Trail of Bits' devcontainer if you are doing security audits or reviewing untrusted repos β their threat model explicitly accounts for malicious code trying to escape the container.
Use Anthropic's official devcontainer if you are on a team that wants a standardised, VS Code-integrated development environment with Claude Code.
