heretic-cli image
Build Heretic agent images and builder (sidecar) images from templates — every agent and toolchain flag, multi-arch builds, the generated Dockerfile stage by stage, and the baked entrypoint
Synopsis
heretic-cli image build [agent/tool flags] [-n|-t|-r|-p|--no-cache|--dry-run|-a]
heretic-cli image build-sidecar <node|python|java|go|rust> [flags]
heretic-cli image generate [--format <template>] [agent/tool flags]
Everything is generated in memory into a temporary directory, built with the docker CLI, and cleaned up afterwards. The templates are embedded in the executable, so nothing has to ship alongside it.
image build
Agent types
--agent <type> is repeatable; all expands to every type.
--agent | Installed npm packages |
|---|---|
claude (default) | @anthropic-ai/claude-code, mcp-remote |
copilot | @github/copilot |
opencode | opencode-ai |
gemini | @google/gemini-cli, mcp-remote |
An invalid value fails with Invalid agent type '<x>'. Use: claude, copilot, opencode, gemini, or all. Duplicates are removed and the list is sorted.
Image naming
| Situation | Resulting image |
|---|---|
one agent, claude | <name>:<tag> — default heretic-agent:latest |
| one agent, other type | <name>-<agent>:<tag>, e.g. heretic-agent-gemini:latest |
several agents with --combined | a single <name>:<tag> containing every CLI |
--registry <reg> | prefixes all of the above: <reg>/<name>:<tag> |
Several agents without --combined builds them sequentially — one image each — and prints the list at the end. With --combined the AGENT_TYPE build arg becomes combined, and you select the agent at run time with -e AGENT_TYPE=<agent>.
Toolchain flags
Shared by build and generate.
| Flag | Effect | Default |
|---|---|---|
--base <image> | base image | node:22-bookworm-slim |
--with-python, --python-version <ver> | Python plus pip, poetry, pytest, black, ruff, mypy | off, 3.13 |
--with-node, --node-version <ver> | Node.js (npm, yarn, pnpm via corepack) | auto, 22 |
--with-go, --go-version <ver> | Go toolchain from go.dev | off, 1.23.4 |
--with-java, --java-version <ver> | Eclipse Temurin JDK, Maven 3.9.6, Gradle 8.5 | off, 21 |
--with-rust, --rust-version <ver> | rustup, rustfmt, clippy | off, stable |
--with-docker | Docker CLI and compose plugin (DinD/DooD) | off |
--with-github-cli / --no-github-cli | GitHub CLI (gh) | on |
--with-all | python + node + go + java + rust + docker + github-cli | off |
--agent-user, --agent-uid, --agent-gid | container user | agent, 1000, 1000 |
Passing a --*-version flag implies the matching --with-*. Node is special: with a node:* base image corepack is simply enabled, otherwise Node is installed unconditionally because the agent CLIs need it. An ubuntu:* base additionally installs and generates en_US.UTF-8 locales.
Build flags
| Flag | Meaning |
|---|---|
-n, --name <name> | image name (default heretic-agent) |
-t, --tag <tag> | tag (default latest) |
-r, --registry <reg> | registry prefix, e.g. ghcr.io/acme |
-p, --push | push after the build |
--no-cache | build without the Docker cache |
--dry-run | print the Dockerfile and the entrypoint, build nothing (also skips the Docker availability check) |
-a, --arch <arch> | amd64/x86_64, arm64/aarch64, both/all; anything else means the current platform |
| Condition | Build mode |
|---|---|
no --arch | plain docker build, then docker push when both -p and -r are given |
--arch without -p | docker buildx build --platform <p> --load |
--arch with -p | docker buildx build --platform <p> --push |
Build args always passed: BASE_IMAGE, AGENT_USER, AGENT_UID, AGENT_GID, AGENT_TYPE.
Without --dry-run, Docker must be reachable, otherwise: Docker is not available. Install Docker or use --dry-run to preview the Dockerfile.
Two multi-arch pitfalls
--push without --registry on a single-platform build silently skips the push. And --arch both cannot be combined with buildx --load, so multi-arch images must be pushed (-a both -p -r <registry>).
Examples
# default: claude agent on node:22-bookworm-slim with gh
heretic-cli image build
# preview the Dockerfile and entrypoint without building
heretic-cli image build --dry-run
# python + go tooling, custom name and tag
heretic-cli image build --with-python --python-version 3.12 --with-go -n acme-agent -t dev
# all four agent CLIs in one image
heretic-cli image build --agent all --combined -n heretic-agent -t all
# one image per agent
heretic-cli image build --agent claude --agent gemini
# multi-arch push to GHCR
heretic-cli image build --with-all -r ghcr.io/acme -t 1.0.0 -a both -p
# match your host uid to avoid root-owned files in bind mounts
heretic-cli image build --agent-uid 501 --agent-gid 20
The generated Dockerfile
Order matters — this is what the template produces:
ARG BASE_IMAGE→FROM, plusTARGETARCH,AGENT_USER/UID/GID,AGENT_TYPEargs, OCI labels and base environment- Base packages:
ca-certificates curl git jq xz-utils gnupg unzip openssh-client vim—jqandcurlare required by the sidecar client,openssh-clientby the SSH client - Optional locales, corepack or NodeSource Node, then one block per
--with-*toolchain npm install -gfor the selected agent CLIs- Tool backends: copy
sidecar-execandssh-execinto/opt/sidecar/, create a writable/opt/sidecar/wrappers, and prepend it toPATH - Create the agent user (removing the base image's
nodeuser, which also owns uid 1000) - Create and chown the agent's config directories and
/workspace COPY entrypoint.sh /entrypoint.shandENTRYPOINT ["/entrypoint.sh"]USER ${AGENT_USER},WORKDIR /home/${AGENT_USER}- Runtime environment defaults:
API_TIMEOUT_MS=3000000,GH_TOKEN="",TASK_ID="",STEP_NAME="",REPO_PATH=/workspace,PROMPT_FILE="",AGENT_ARGS="",AGENT_OUTPUT_FORMAT=stream-json CMD ["/bin/bash"]
Baking the ENTRYPOINT is what makes root mode and the tool backends work — without it the wrappers are never generated.
The baked entrypoint
Root mode. When the container runs as uid 0 and HERETIC_RUN_AS_ROOT is set, it exports HOME=/home/<agent-user> and USER=root and prints Running as root (HOME=$HOME). This is what keeps bind-mounted configuration resolvable under heretic-cli run --root.
Tool wrappers. For each configured backend it writes /opt/sidecar/wrappers/<cmd> — but only for commands that are not already on PATH, so a real local toolchain always wins:
node npm npx pnpm yarn node
python python python3 pip pip3 poetry pytest ruff black mypy
java java javac mvn gradle
go go gofmt
rust cargo rustc rustfmt clippy
A runtime present in BUILD_SIDECARS routes to the HTTP builder; otherwise, if SSH_HOST is set, to the SSH host. With no backend configured it does nothing. If a backend is configured but the wrappers directory is not writable it exits 1 with FATAL: /opt/sidecar/wrappers is not writable by <user> (uid <n>); tool backends disabled.
Mode detection.
| Mode | Trigger | Behaviour |
|---|---|---|
| Interactive | PROMPT_FILE empty | prints a banner with detected tool and agent-CLI versions, then exec /bin/bash |
| Workflow | PROMPT_FILE set | configures git credentials from GH_TOKEN/GITHUB_TOKEN, sets the git identity, picks up .githooks, copies agent settings into place, expands AGENT_ARGS (file:<path> → file contents, exec:<cmd> → command output), then runs the agent non-interactively |
AGENT_TYPE | Workflow-mode command |
|---|---|
claude | claude --print --output-format ${AGENT_OUTPUT_FORMAT:-stream-json} <args> "$PROMPT" |
copilot | copilot <args> "$PROMPT" |
opencode | opencode <args> "$PROMPT" |
gemini | gemini <args> "$PROMPT" |
Workflow mode is how these images are used outside heretic-cli run — in CI or an orchestrator:
docker run -it --rm \
-e PROMPT_FILE=/workspace/prompt.md \
-e REPO_PATH=/workspace \
-v "$(pwd)":/workspace \
heretic-agent:latest
image build-sidecar <runtime>
Builds a builder image: a statically compiled Go exec-server on top of a runtime base.
heretic-cli image build-sidecar <node|python|java|go|rust>
-n, --name <name> default heretic-builder-<runtime>
-t, --tag <tag> default latest
-r, --registry <reg>
--runtime-version <v> toolchain version
--go-builder <image> golang image for the exec-server build stage
--port <port> port baked into ENV/EXPOSE (default 8080)
-p, --push
--no-cache
--dry-run print the Dockerfile only
-a, --arch <amd64|arm64|both>
| Runtime | Base image | Extra tools |
|---|---|---|
node | node:<v>-bookworm-slim (22) | corepack (yarn, pnpm) |
python | python:<v>-slim-bookworm (3.13) | poetry, pytest, ruff, black, mypy |
java | eclipse-temurin:<v>-jdk (21) | maven, gradle |
go | golang:<v>-bookworm (1.23) | — |
rust | rust:<v>-bookworm | rustfmt, clippy |
The image runs as a non-root builder user, exposes the port, and declares a port-agnostic HEALTHCHECK that heretic uses as the readiness gate. On success the command prints the profile snippet to paste:
tool_backends:
sidecars:
- runtime: node
image: heretic-builder-node:latest
See Sidecars for the wiring, the BUILD_SIDECARS contract and the security posture.
image generate
Prints one artifact to stdout — pipe it, diff it, or commit it.
--format | Output |
|---|---|
dockerfile (default) | the agent Dockerfile for the given flags |
entrypoint | the embedded entrypoint.sh |
ssh-exec | the SSH backend client script |
sidecar-exec | the HTTP build-sidecar client script |
sidecar-dockerfile | a builder Dockerfile; requires --runtime (plus optional --runtime-version, --go-builder, --port) |
exec-server | the vendored Go exec-server source |
heretic-cli image generate --with-all > Dockerfile
heretic-cli image generate --format entrypoint > entrypoint.sh
heretic-cli image generate --format sidecar-dockerfile --runtime python --runtime-version 3.12
An invalid value fails with Invalid format '<x>'. Use: dockerfile, entrypoint, ssh-exec, sidecar-exec, sidecar-dockerfile, exec-server; an invalid runtime with Invalid sidecar runtime '<x>'. Use one of: node, python, java, go, rust.
Gotchas
- The Java block pins the JDK, Maven 3.9.6 and Gradle 8.5 by URL — versions Temurin has moved will 404; prefer a JDK base image for anything exotic.
--python-versionuses the deadsnakes PPA, which only exists on Ubuntu bases. On the default Debian base use plain--with-python(distro default) or apython:base image.- Rust is installed under root's home and copied into the agent's home; the agent's
PATHcomes from.bashrc, so non-login shells may not seecargo. - The wrappers directory must be owned by
AGENT_UID; overriding the container user later (extra.user) can trip the writability check. - Images built before the entrypoint was baked in do not support root mode or tool backends. Rebuild them, or use
heretic-cli run --root, which bind-mounts the current entrypoint. imagenever touches your profiles — set the resulting image name yourself withheretic-cli agents edit <name>.
Next Steps
- Docker Images — choosing an image for a profile
- Sidecars — build toolchains in sibling containers
- SSH Host Access — the remote-host alternative