heretic-cli run
Start agent containers — every flag, the resolution pipeline, what gets mounted and injected, sessions, container naming, root mode, build sidecars, and exit codes
Synopsis
heretic-cli run <agent-name> [command...]
-d, --detach run the container in the background
-s, --session <name> session name (default: "default")
--mcp <value> JSON string or path to a .json file (array of servers)
--root run as root, keeping HOME=/home/agent
--sidecar <runtime> repeatable: node|python|java|go|rust
--builder-image <rt>=<image> repeatable builder image override
--disable-sidecars disable all build sidecars for this run
heretic-cli <agent-name> [-d] [-s <name>] [--root] [--sidecar <rt>]
[--builder-image <rt>=<img>] [--disable-sidecars] [-- <command...>]
The project directory is always the current working directory — run mounts it, hashes it into the container name, and stores it in the heretic.project label.
Options
| Flag | Description |
|---|---|
-d, --detach | start and return; prints the container ID and the attach hint. With the compose runner this becomes docker compose up -d. |
-s, --session <name> | per-project namespace: session directory, container name, heretic.session label, compose project name, sidecar network. Names are sanitized ([^a-zA-Z0-9_-] → -). |
--mcp <value> | replaces the profile's mcp array for this run. Value is a path to a .json file or an inline JSON array of server objects. |
--root | one-off extra.run_as_root: true. Only applied when passed, so it never clobbers a profile value. |
--sidecar <runtime> | replaces the profile's sidecar list for this run; each image defaults to heretic-builder-<runtime>:latest. |
--builder-image <rt>=<image> | retargets one runtime's image, including sidecars declared in the profile. |
--disable-sidecars | wins over --sidecar — sets an empty sidecar list. |
command... | replaces the profile's command (the container Cmd). Its own flags are passed through untouched. |
Resolution pipeline
1. collect CLI overrides --mcp, --root, --sidecar / --builder-image / --disable-sidecars
2. resolveConfig() global profile → local override → CLI overrides
+ ${VAR} interpolation, secret scripts, mcp_file merge
+ post-merge validation
3. apply --builder-image also to profile-declared sidecars
4. reject sidecars on runner: custom
5. create the runner docker | compose | custom
6. runner.start() sidecars first, then the agent container
Info-level output: Resolving config for '<agent>'…, Using global profile: <agent>, Starting agent '<agent>'…. Local-override detection is logged at debug level (-V).
Sessions
heretic-cli run claude -s feature-a
heretic-cli run claude -s feature-b # runs at the same time, separate ~/.claude state
Two runs of the same session in the same directory collide: the second one stops and removes the first container. Sessions do not isolate the workspace — both sessions edit the same files. See Sessions.
Container naming
heretic-<agent>-<session>-<hash8>
hash8 is the first 8 hex characters of sha256(<absolute project dir>), so the same profile in two checkouts produces two different containers.
MCP override
heretic-cli run claude --mcp ./servers.json
heretic-cli run claude --mcp '[{"name":"fs","command":"npx","args":["-y","@modelcontextprotocol/server-filesystem","/workspace"]}]'
--mcp takes an array, not an mcpServers map
--mcp expects the McpServer[] shape. The {"mcpServers": {…}} / {"servers": {…}} map formats are what heretic-cli agents mcp and the profile's mcp_file accept. Bad input fails with Invalid --mcp value: MCP config must be a JSON array of server objects. --mcp is also not available in the shorthand form.
Root mode
heretic-cli run claude --root
heretic-cli run --root claude # identical — the flag is hoisted
With root mode the runner forces User: root, injects HERETIC_RUN_AS_ROOT=1, and bind-mounts the CLI's embedded entrypoint.sh over the image's /entrypoint.sh, so the flag works even on images built before the feature existed. The entrypoint then exports HOME=/home/agent and USER=root, which keeps every bind-mounted config (Claude settings, .claude.json, MCP, SSH keys) resolvable.
docker exec shows HOME=/root
docker exec starts a fresh login that does not inherit the entrypoint's exports. The actual agent session (PID 1) has HOME=/home/agent.
Build sidecars
heretic-cli run claude --sidecar node --sidecar python
heretic-cli run claude --builder-image node=ghcr.io/me/builder-node:1.2.3
heretic-cli run claude --disable-sidecars
Builders start before the agent, on a private per-run network, and the agent is wired to them through BUILD_SIDECARS. If a builder never becomes healthy the whole run is aborted and everything created is torn down. Full details: Sidecars.
Runner behaviour
| Runner | What run does |
|---|---|
docker (default) | pings the daemon, pulls the image if missing, starts sidecars, creates the container, removes a stale container with the same name, then attaches (interactive) or returns the ID (detached) |
compose | generates .heretic/temp/<session>/compose.yaml and runs docker compose -f <file> -p heretic-<agent>-<session> up [-d] |
custom | runs your .heretic/cli/compose.yaml verbatim, exporting resolved secrets and env into the compose process environment |
Interactive runs shell out to docker attach, so the docker CLI must be installed — the daemon socket alone is not enough. Right after start the runner fixes ownership of the mounted agent home (chown -R agent:agent), which is what makes Docker Desktop bind mounts writable for uid 1000.
See Runners for the full comparison.
What lands in the container
Environment — resolved secrets (docker runner: only those also named in env) → profile env → transformations → GitHub/Copilot tokens → git.* → HERETIC_RUN_AS_ROOT → BUILD_SIDECARS / SIDECAR_ENV_PASSTHROUGH → SSH_*. Variable names are logged at info level; values only at debug level, masked.
Mounts
profile volumes (e.g. ${CWD} → /workspace)
.heretic/temp/<session>/entrypoint.sh → /entrypoint.sh:ro (root mode only)
<ssh.key_path> → /home/agent/.ssh/id_rsa:ro
generated .mcp.json → /workspace/.mcp.json (or both .copilot paths)
/var/run/docker.sock → /var/run/docker.sock (dind: true)
.heretic/temp/<session> → /home/agent/.claude and /root/.claude
.heretic/temp/<session>/.claude.json → /home/agent/.claude.json and /root/.claude.json
Labels — heretic.managed=true, heretic.agent, heretic.project, heretic.session, plus heretic.network when sidecars are used. extra.labels is applied last and can override them.
Examples
heretic-cli run claude # interactive, session "default"
heretic-cli claude # identical (shorthand)
heretic-cli run claude -s feature-x # separate session
heretic-cli run claude -d && heretic-cli ps # background, then list
heretic-cli run claude -- bash -lc 'npm ci && npm test'
heretic-cli run claude --root
heretic-cli run claude --sidecar node --sidecar python
heretic-cli run claude --mcp ./mcp-servers.json
heretic-cli -V run claude # verbose (before the subcommand)
Errors and exit codes
| Situation | Message | Exit |
|---|---|---|
| unknown profile | Profile '<x>' not found. + Run 'heretic-cli agents list' … | 1 |
| invalid profile YAML/shape | Invalid profile '<x>': … | 1 |
| override for another profile | Local config extends 'a' but loading profile 'b' | 1 |
| all-comments local override | Failed to load local config from …: Invalid config: expected an object | 1 |
| post-merge validation | Config validation failed: <errors> | 1 |
| Docker unavailable | Docker is not available. Start Docker and try again. | 1 |
bad --mcp | Invalid --mcp value: … | 1 |
bad --builder-image | --builder-image expects <runtime>=<image>, got '…' | 1 |
sidecars + runner: custom | Build sidecars require the 'docker' or 'compose' runner … | 1 |
| builder never healthy | build sidecar '<name>' did not become healthy within <n>s | 1 |
| container finished | Container exited with code <n> (logged when non-zero) | container's code |
Gotchas
- A local override replaces the whole
volumesarray — adding one mount means re-listing all of them. - Volume sources must be absolute after interpolation; use
${CWD}and${HOME}. - Interactive mode needs a TTY. In CI use
-dplusdocker logs, or pass an explicit command. - The session directory is created with mode
0777so any container uid can write to it; treat it as scratch —.mcp.json,settings.json,compose.yamlandentrypoint.share regenerated every run. - There is no
--dry-run; preview withheretic-cli agents show <name> --resolvedorheretic-cli local-validate <name>.
Next Steps
- Sessions — parallel instances
- Runners — docker vs compose vs custom
- Sidecars — build toolchains in sibling containers
- Merge Rules — how the layers combine