---
title: heretic-cli image
description: "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"
canonical: https://giglabo.com/heretic/docs/heretic-cli/commands/image
locale: en
---

# heretic-cli image

> Markdown twin of https://giglabo.com/heretic/docs/heretic-cli/commands/image
> Site structure and the full page list for agents: https://giglabo.com/llms.txt

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.`

> **Warning: 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

```bash
# 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:

1. `ARG BASE_IMAGE` → `FROM`, plus `TARGETARCH`, `AGENT_USER/UID/GID`, `AGENT_TYPE` args, OCI labels and base environment
2. Base packages: `ca-certificates curl git jq xz-utils gnupg unzip openssh-client vim` — `jq` and `curl` are **required** by the sidecar client, `openssh-client` by the SSH client
3. Optional locales, corepack or NodeSource Node, then one block per `--with-*` toolchain
4. `npm install -g` for the selected agent CLIs
5. **Tool backends**: copy `sidecar-exec` and `ssh-exec` into `/opt/sidecar/`, create a writable `/opt/sidecar/wrappers`, and prepend it to `PATH`
6. Create the agent user (removing the base image's `node` user, which also owns uid 1000)
7. Create and chown the agent's config directories and `/workspace`
8. `COPY entrypoint.sh /entrypoint.sh` and **`ENTRYPOINT ["/entrypoint.sh"]`**
9. `USER ${AGENT_USER}`, `WORKDIR /home/${AGENT_USER}`
10. 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`
11. `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:

```bash
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:

```yaml
tool_backends:
  sidecars:
    - runtime: node
      image: heretic-builder-node:latest
```

See [Sidecars](https://giglabo.com/heretic/docs/heretic-cli/configuration/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 |

```bash
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-version` uses the deadsnakes PPA, which only exists on Ubuntu bases. On the default Debian base use plain `--with-python` (distro default) or a `python:` base image.
- Rust is installed under root's home and copied into the agent's home; the agent's `PATH` comes from `.bashrc`, so non-login shells may not see `cargo`.
- 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.
- `image` never touches your profiles — set the resulting image name yourself with `heretic-cli agents edit <name>`.

## Next Steps

- [Docker Images](https://giglabo.com/heretic/docs/heretic-cli/configuration/docker-images) — choosing an image for a profile
- [Sidecars](https://giglabo.com/heretic/docs/heretic-cli/configuration/sidecars) — build toolchains in sibling containers
- [SSH Host Access](https://giglabo.com/heretic/docs/heretic-cli/configuration/ssh) — the remote-host alternative

## Related

- HTML version of this page: https://giglabo.com/heretic/docs/heretic-cli/commands/image
- Site map for agents: https://giglabo.com/llms.txt
