Run agents in a sandboxed container — ready to drop into any project.
Harness conveniently wraps Docker around three open-source coding agents — pi, opencode,
and hermes — so you can point one at a directory (or file) without giving it access to your entire machine.
Documentation: capotej.github.io/harness
- Sandboxed by default — capability-dropped container with
no-new-privileges; the agent only sees the directory (or file) you mount. - Three agents, one CLI — switch between
pi,opencode, andhermeswith-a. Same flags, same flow. - Supply-chain hardened — the image is signed and verified with cosign and SLSA provenance on every run; dependencies installed inside the container are always pinned and verified where possible and a 7-day "cooldown" is used to mitigate supply-chain attacks.
- Local-first — defaults to LM Studio with
gemma-4-e4b. Drop in an--env-fileto use Anthropic, OpenRouter, OpenAI, Gemini, and others. - Stateful or one-shot — interactive runs persist agent state under
$XDG_DATA_HOME/harness/<project>/<agent>/(defaults to~/.local/share/harness/); one-shot prompts (-por piped stdin) stay ephemeral. - User skills — automatically mounts
~/.agents/skillsand~/.claude/skillsinto the container so agents can discover and use custom skills. Disable with--no-skills. - Zero install —
npx @capotej/harnessjust works.
Docker is required. By default, harness uses LM Studio locally:
lms daemon up
lms get google/gemma-4-e4bThe container is preconfigured to use gemma-4-e4b via LM Studio's local API.
You can also specify a different local model with -m. HuggingFace-style names with slashes (e.g. qwen/qwen3.5-9b) work correctly in local mode:
npx @capotej/harness -m "qwen/qwen3.5-9b" -p "write a fizzbuzz in Go"If you pass an API key for a supported provider via --env-file, pi will use that provider instead of the local LM Studio setup. Supported keys:
| Provider | Environment Variable |
|---|---|
| Anthropic | ANTHROPIC_API_KEY |
| OpenRouter | OPENROUTER_API_KEY |
| OpenAI | OPENAI_API_KEY |
| Google Gemini | GEMINI_API_KEY |
| Mistral | MISTRAL_API_KEY |
| Groq | GROQ_API_KEY |
| Cerebras | CEREBRAS_API_KEY |
| xAI | XAI_API_KEY |
| Hugging Face | HF_TOKEN |
See the full list of supported providers for more options. When using LM Studio locally, 16k context is sufficient.
opencode uses LM Studio by default. Pass --env-file to switch to cloud mode — the agent auto-detects the provider from whichever API key is in the file:
echo 'OPENROUTER_API_KEY=sk-...' > .env
npx @capotej/harness -e .env -p "write me a fizzbuzz in Go"That's it. Your current directory is mounted at /workspace inside the container and the agent works against it.
# One-shot prompt
npx @capotej/harness -p "write me a fizzbuzz in Go"
# Pipe via stdin
echo "write me a fizzbuzz in Go" | npx @capotej/harness
# Interactive session (no -p, no piped stdin) — state persists under XDG data dir
npx @capotej/harness
# Use a cloud provider via env file
npx @capotej/harness -e .env -p "add a login endpoint"
# Override the model
npx @capotej/harness -m anthropic/claude-sonnet-4-5 -p "refactor the auth module"
# Mount a single file instead of the whole directory
npx @capotej/harness -f ./script.py -p "add type hints"
# Switch agents
npx @capotej/harness -a opencode -p "write me a fizzbuzz in Go"
npx @capotej/harness -a hermes -e .env -p "add tests"npx, bunx, and pnpm dlx are interchangeable. Or install globally:
npm install -g @capotej/harness
# or
pnpm add -g @capotej/harness
# or
bun add -g @capotej/harnessPick an agent with -a. Default is pi.
pi defaults to LM Studio with google/gemma-4-e4b (16k context is enough). Pass an --env-file containing any of the keys below and pi switches to that provider:
| Provider | Environment Variable |
|---|---|
| Anthropic | ANTHROPIC_API_KEY |
| OpenRouter | OPENROUTER_API_KEY |
| OpenAI | OPENAI_API_KEY |
| Google Gemini | GEMINI_API_KEY |
| Mistral | MISTRAL_API_KEY |
| Groq | GROQ_API_KEY |
| Cerebras | CEREBRAS_API_KEY |
| xAI | XAI_API_KEY |
| Hugging Face | HF_TOKEN |
See the full provider list. The -m flag is forwarded directly.
opencode defaults to LM Studio in local mode. Pass --env-file to enter cloud mode — the agent auto-detects the provider from whichever API key is in the file (ZAI_API_KEY, OPENROUTER_API_KEY, ANTHROPIC_API_KEY, etc.). The -m flag takes a bare model name; the provider prefix is added for you.
npx @capotej/harness -a opencode -e .env -p "refactor the auth module"
npx @capotej/harness -a opencode -e .env -m anthropic/claude-sonnet-4-5 -p "add tests"To pass env vars but stay in local mode, use --local:
npx @capotej/harness -a opencode -e .env --local -p "refactor the auth module"When using LM Studio locally, set the model's context length to at least 32k tokens.
hermes by NousResearch supports many providers. Pass --env-file to enter cloud mode — the agent auto-detects the provider from whichever API key is in the file. Use a provider/model for -m:
npx @capotej/harness -a hermes -e .env -m anthropic/claude-sonnet-4-5 -p "add tests"
npx @capotej/harness -a hermes -e .env -m openrouter/auto -p "add tests"Common keys: ANTHROPIC_API_KEY, OPENROUTER_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY, and others. LM Studio context length should be at least 64k tokens.
Harness layers protections at runtime, image, and dependency level.
Each run starts the container with:
--cap-drop=ALL --cap-add=NET_RAW— minimal capability set--security-opt no-new-privileges:true— block privilege escalation--security-opt seccomp=...— inline seccomp profile blockssocket(AF_ALG)to prevent kernel crypto API access (a known container escape vector)- Only your mounted directory (or single file with
-f) is visible to the agent
By default, harness verifies that the container image was signed by the official CI workflow and carries a valid SLSA provenance attestation. This requires cosign:
brew install cosignVerified digests are cached at ~/.cache/harness/cosign-verified.json so verification only runs once per image. Skip with --no-verify (or by setting HARNESS_IMAGE_TAG, which implies skip):
npx @capotej/harness --no-verify -p "write me a fizzbuzz in Go"The image build enforces a 7-day cooldown on dependency resolution — a guard against supply-chain compromises that are typically discovered and yanked within hours.
- pnpm:
PNPM_MINIMUM_RELEASE_AGE=10080(minutes) via environment variable - uv:
--exclude-newerset to 7 days ago at image build time
The cooldown applies to transitive dependencies too. Older packages install normally.
Interactive runs (no -p and no piped stdin) store persistence data at $XDG_DATA_HOME/harness/<project>/<agent>/ (defaults to ~/.local/share/harness/). The <project> segment is the working directory path with / replaced by _ and the home prefix stripped. This lets agents resume sessions, skip database migrations on repeat runs, and retain memories across invocations. Per-agent mise tool data and trust settings are persisted at <persist-root>/mise/ and <persist-root>/mise-state/ respectively. For the pi adapter, extensions/skills installed via npm install -g are persisted at <persist-root>/npm/, avoiding re-downloads on every boot.
One-shot runs (-p or piped stdin) are implicitly ephemeral — no persistence data is created. Use --ephemeral to force-disable persistence on interactive runs.
If an old .harness/ directory exists in your working directory, harness will emit a deprecation warning with migration instructions.
| Flag | Alias | Description |
|---|---|---|
--prompt |
-p |
Pass a prompt directly to the agent |
--env-file |
-e |
Load environment variables into the container |
--file |
-f |
Mount a single file instead of the current directory |
--model |
-m |
Override the model used by the agent |
--agent |
-a |
Select agent: pi, opencode, hermes (default: pi) |
--volumes |
-v |
Additional volume mount (host:container[:opts]); may be repeated |
--no-verify |
Skip cosign signature and provenance verification | |
--no-skills |
Disable mounting user skills directories (~/.agents/skills, ~/.claude/skills) |
|
--ephemeral |
Disable session persistence (implied by -p and piped stdin) |
|
--local |
Force local mode even with -e (use LM Studio / local defaults) |
|
--help |
-h |
Show help |
| Variable | Description |
|---|---|
HARNESS_IMAGE_TAG |
Override the Docker image tag (defaults to the package version). Setting this implies --no-verify. |
- pi —
-mis passed straight to the binary as--model. - opencode —
-mis passed via theOPENCODE_MODELenv var. Without-e, uses LM Studio locally. With-e, enters cloud mode and auto-detects the provider from whichever API key is in the env file. Use--localto force local mode even with-e. - hermes —
-mis passed as--modelinprovider/modelform. Without-e, uses local config. With-e, enters cloud mode and auto-detects from env vars. Use--localto force local mode even with-e.
You can run hermes as a long-running "claw" — a persistent agent process reachable over a messaging gateway (e.g. Telegram). Two deployment targets are documented:
Link your local checkout globally:
pnpm link --global
# unlink with:
pnpm unlink --global @capotej/harnessmake imageBuilds ghcr.io/capotej/harness with Debian stable-slim, Node.js v24, git, @mariozechner/pi-coding-agent, opencode-ai, hermes-agent, fd, ripgrep, jq, and curl. The hermes variant also includes the MCP Python SDK for connecting to MCP servers, and faster-whisper for local speech-to-text.
The base image is pinned by manifest-list digest (the OCI image index, not a per-platform manifest) for reproducible multi-arch builds. To bump it:
docker buildx imagetools inspect debian:stable-slim --format '{{.Manifest.Digest}}'pnpm lint # all
pnpm lint:ts # Biome
pnpm lint:md # markdownlint
pnpm lint:sh # shellcheck
pnpm lint:docker # hadolint
pnpm lint:actions # actionlint
pnpm format # auto-format with Biomeshellcheck, hadolint, and actionlint are system binaries. Install with mise (recommended):
mise installOr install manually:
brew install shellcheck hadolint actionlint
