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: harness.boldblack.ai/docs
- 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. - Context files — automatically mounts
~/.agents/AGENTS.mdand~/.claude/CLAUDE.mdinto the agent's context directory so cross-agent rules apply inside the container. Disable with--no-context-files(-nc). - Zero install —
npx @boldblackai/harnessjust works.
A container runtime is required. By default harness uses Docker; on macOS 26 / Apple Silicon, Apple's native container CLI is preferred automatically if installed — see Container runtime. With the default runtime and 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 @boldblackai/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 @boldblackai/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 @boldblackai/harness -p "write me a fizzbuzz in Go"
# Pipe via stdin
echo "write me a fizzbuzz in Go" | npx @boldblackai/harness
# Interactive session (no -p, no piped stdin) — state persists under XDG data dir
npx @boldblackai/harness
# Use a cloud provider via env file
npx @boldblackai/harness -e .env -p "add a login endpoint"
# Override the model
npx @boldblackai/harness -m anthropic/claude-sonnet-4-5 -p "refactor the auth module"
# Mount a single file instead of the whole directory
npx @boldblackai/harness -f ./script.py -p "add type hints"
# Switch agents
npx @boldblackai/harness -a opencode -p "write me a fizzbuzz in Go"
npx @boldblackai/harness -a hermes -e .env -p "add tests"npx, bunx, and pnpm dlx are interchangeable. Or install globally:
npm install -g @boldblackai/harness
# or
pnpm add -g @boldblackai/harness
# or
bun add -g @boldblackai/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 @boldblackai/harness -a opencode -e .env -p "refactor the auth module"
npx @boldblackai/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 @boldblackai/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 @boldblackai/harness -a hermes -e .env -m anthropic/claude-sonnet-4-5 -p "add tests"
npx @boldblackai/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, GLM_API_KEY (Z.AI), and others. LM Studio context length should be at least 64k tokens.
Multiple API keys in one env file. With --env-file, each agent auto-detects a provider from the keys present. If the file contains keys for more than one provider (e.g. OPENROUTER_API_KEY and GLM_API_KEY), the agent may not pick the one you expect — pi and opencode follow their own detection order. For one-shot runs, use a single-provider env file, or select the agent and model explicitly (e.g. harness -a hermes -e .env -m zai/glm-4.7 -p "...").
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
Running harness from your home directory is blocked by default, since that would mount all of $HOME into the container — exposing dotfiles, credentials, and SSH keys to the agent and giving it a noisy, unfocused workspace. If the resolved working directory equals your home, harness exits with an error. Pass --mount-entire-home to opt in for the rare case where mounting all of $HOME is genuinely intended. --file mode is unaffected (it mounts a single file, not the working directory).
These hardening flags are docker-specific. When running under Apple's container CLI (auto-detected on macOS 26 / Apple Silicon), --security-opt is not applied (apple/container workloads are microVMs with their own guest kernel, so the seccomp profile's host-kernel role is subsumed by the VM boundary); capability restrictions remain. See Container runtime.
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 @boldblackai/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_CONFIG_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. The container's ~/.config is persisted at <persist-root>/../xdg_config (one level above the per-agent root, i.e. $XDG_DATA_HOME/harness/<project>/xdg_config), so tool configs (e.g. jj) survive across runs and are shared by every agent working in the same project. For the opencode adapter the existing per-agent .config/opencode mount nests inside this project-level one. 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.
By default the agent container runs on the default bridge with unrestricted outbound access. --network lets you attach it to an existing network instead — for example a network wired to an existing corporate proxy — bringing your own egress topology.
harness --network corp-proxy-netThe value is forwarded verbatim to the container runtime (docker run --network <name> / container run --network <name>); no harness-side validation beyond non-empty. --network host proceeds with a warning that host networking removes network isolation.
Tier 2 — --egress <allowlist> managed enforcement (per RFC: opt-in egress allowlisting). Harness creates a per-run --internal network (no external route) plus an egress gateway container (squid, CONNECT-only) that is the only container with a route off-host. The agent container reaches the internet only through the gateway, and the gateway forwards only allowlisted destination hostnames. Allowlist syntax:
--egress "api.anthropic.com,api.openai.com,openrouter.ai,.amazonaws.com"
--egress @/etc/harness/egress-allowlist.txt # one entry per line, # comments
--egress none # local-only: no gateway, host reachable
| Entry | Meaning |
|---|---|
host |
exact hostname, HTTPS (443) via CONNECT |
.domain or *.domain |
any host under domain (e.g. .amazonaws.com covers regional endpoints) |
host.docker.internal[:port] |
host-local destination: direct connection, never proxied (port advisory in v1) |
host:port |
rejected in v1 — the raw-TCP extension is deferred (see the RFC) |
HTTPS_PROXY / HTTP_PROXY / NO_PROXY (and NODE_USE_ENV_PROXY=1 — a Node ≥24 built-in that makes fetch/undici honor the proxy env vars) are injected after --env-file, so they take precedence over same-key values in a user env file. The boundary is the network topology, not the env vars — an agent that ignores the proxy env vars can still only reach the gateway. On Linux, harness maps host.docker.internal to the egress network's own gateway to keep the host reachable from the internal network (Docker Desktop provides host.docker.internal natively). The egress subnet pool defaults to 192.0.2.0/24 split into /29s; override with HARNESS_EGRESS_SUBNET=<cidr> when it collides with your networks. --egress is docker-only in v1 (the apple/container runtime fails closed with an error) and mutually exclusive with --network.
Local-only mode pairs naturally with local mode: harness --local --egress none runs the agent against host.docker.internal:1234 (LM Studio) with no route anywhere else — unlike docker run --network none, which would break local-LLM usage too. On Linux the host is reached via the egress network's own gateway (harness maps host.docker.internal to it automatically).
| Flag | Alias | Description |
|---|---|---|
--prompt |
-p |
Pass a prompt directly to the agent |
--env-file |
-e |
Load environment variables into the container; may be repeated |
--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 |
--port |
Publish a container port to the host ([host-ip:]host-port:container-port[/protocol]); may be repeated |
|
--no-verify |
Skip cosign signature and provenance verification | |
--no-skills |
Disable mounting user skills directories (~/.agents/skills, ~/.claude/skills) |
|
--no-context-files |
-nc |
Disable mounting global context files (~/.agents/AGENTS.md, ~/.claude/CLAUDE.md) |
--ephemeral |
Disable session persistence (implied by -p and piped stdin) |
|
--local |
Force local mode even with -e (use LM Studio / local defaults) |
|
--mount-entire-home |
Allow running from your home directory (mounts all of $HOME as the workspace) |
|
--network |
Attach the container to an existing network (e.g. one wired to a corp proxy); see Egress control | |
--egress |
Restrict outbound traffic to an allowlist (hosts, .suffix domains, @file, or none for local-only); see Egress control |
|
--help |
-h |
Show help |
| Variable | Description |
|---|---|
HARNESS_IMAGE_TAG |
Override the Docker image tag (defaults to the package version). Setting this implies --no-verify. |
HARNESS_CONTAINER_RUNTIME |
Container runtime: auto (default — on macOS, prefers Apple's container CLI if on PATH; docker everywhere else), docker (force docker), or apple (deprecated explicit alias; warns). |
HARNESS_EGRESS_SUBNET |
Override the egress network subnet (IPv4 CIDR; default pool 192.0.2.0/24 split into /29s). |
XDG_DATA_HOME |
Override the base directory for persistence data (defaults to ~/.local/share). |
XDG_CACHE_HOME |
Override the base directory for the cosign cache (defaults to ~/.cache). |
By default harness auto-detects the container runtime: on macOS, if Apple's native container CLI (v1.0.0+) is on PATH, it is preferred; on every other platform docker is used. Auto-detection is gated to macOS because apple/container ships for macOS only — a binary named container on a Linux PATH is an unrelated tool and is ignored. No configuration is needed.
brew install container # install Apple's container CLI (v1.0.0+)
container system start # one-time: start the container system service
container system kernel set --recommended --arch arm64 # one-time on Apple Silicon (required before first run)
harness -p "write me a fizzbuzz in Go"On arm64 Macs, container system kernel set --recommended is required — without it the first harness run fails with default kernel not configured for architecture arm64. See apple/container for details.
To force docker even when container is available, set:
export HARNESS_CONTAINER_RUNTIME=dockerHARNESS_CONTAINER_RUNTIME=apple (the pre-auto-detect opt-in) still works but is deprecated: it selects the apple runtime explicitly, prints a deprecation warning, and will be removed in a future release.
Image verification (cosign + SLSA provenance) works identically under both runtimes; the verified-digest cache is keyed by digest, so a digest verified under one runtime is a cache hit under the other.
Security note. When running under Apple's container CLI, harness does not apply the --security-opt no-new-privileges and --security-opt seccomp=... flags it uses under docker, because apple/container has no --security-opt option. This is not a security regression: each apple/container workload is a microVM with its own ephemeral guest kernel (Apple Virtualization framework), so the block-af-alg.json profile's host-kernel role — blocking socket(AF_ALG) — is subsumed by the VM boundary itself (hardware-assisted isolation, strictly stronger than a syscall filter). Capability restrictions (--cap-drop=ALL --cap-add=NET_RAW) are supported and stay on. Only ro/readonly is honored for volume options under the apple path (SELinux relabel flags like :Z are meaningless under macOS virtiofs).
Optional tuning — resource limits. Apple's container CLI reads defaults from ~/.config/container/config.toml. Create this file to control CPU and memory allocation for all containers (see container-system-config.md for the full schema):
# example values; adjust to your machine
[container]
cpus = 8
memory = "4g"Container → host (host.docker.internal). Docker Desktop provides host.docker.internal automatically; Apple's container CLI does not. Local-mode agents (pi, opencode, hermes without -e) reach LM Studio and other host services via that hostname. One-time setup on Apple Silicon (requires administrator):
sudo container system dns create host.docker.internal --localhost 203.0.113.113Harness prints a reminder on HARNESS_CONTAINER_RUNTIME=apple runs if this mapping is missing. See apple/container — access a host service.
- 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). Three deployment targets are documented:
Link your local checkout globally:
pnpm link --global
# unlink with:
pnpm unlink --global @boldblackai/harnessmake imageBuilds ghcr.io/boldblackai/harness with Debian stable-slim, Node.js v24, git, @earendil-works/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, faster-whisper for local speech-to-text, and kokoro-onnx for local text-to-speech.
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