---
title: Agent Profiles
description: "Complete agent profile YAML reference — every field, type, default and merge behaviour, plus environment transformations, automatic mounts, container labels and onboarding seeding"
canonical: https://giglabo.com/heretic/docs/heretic-cli/configuration/agent-profiles
locale: en
---

# Agent Profiles

> Markdown twin of https://giglabo.com/heretic/docs/heretic-cli/configuration/agent-profiles
> Fetch this instead of the HTML page: same content, a fraction of the bytes.
> Site structure and the full page list for agents: https://giglabo.com/llms.txt

Complete agent profile YAML reference — every field, type, default and merge behaviour, plus environment transformations, automatic mounts, container labels and onboarding seeding

A profile is one YAML file at `~/.heretic/agents/<name>.yaml`. The filename is the profile name. Local overrides in `.heretic/cli/<name>.yaml` use the **same schema** plus `extends:`.

## Minimal profile

```yaml
image: giglabo/claude-heretic:latest
runner: docker
```

Everything else has a default. See [Docker Images](https://giglabo.com/heretic/docs/heretic-cli/configuration/docker-images) for image options and [`heretic-cli image build`](https://giglabo.com/heretic/docs/heretic-cli/commands/image) for building your own.

## Top-level fields

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `image` | string | **yes** | — | Docker image; trimmed; pulled automatically when missing locally |
| `runner` | `docker` / `compose` / `custom` | **yes** for validation | `docker` when resolving | `agents validate` errors when absent, the resolver still defaults it |
| `agent_type` | `claude` / `aider` / `copilot-cli` / `generic` | no | `claude` | drives MCP mount paths and which home directory is mounted |
| `provider` | `anthropic` / `thirdparty` / `copilot` | no | inferred: `copilot-cli` → `copilot`, otherwise `anthropic` | only `copilot` receives Copilot token injection |
| `volumes` | array of mounts | no | `[]` | sources must be **absolute after interpolation** |
| `env` | map of string → string | no | `{}` | values must be strings; `${VAR}` interpolated |
| `workdir` | string | no | `""` | container working directory |
| `command` | string or array | no | `[]` | normalised to an array; overrides the image `CMD` |
| `interactive` | boolean | no | `true` | keeps stdin open |
| `tty` | boolean | no | `true` | allocates a TTY |
| `extra` | object | no | `{}` | Docker knobs — see below |
| `compose` | object | no | — | only used by the `compose` runner (warning otherwise) |
| `ssh` | object | no | — | SSH tool-execution backend |
| `tool_backends` | object | no | — | HTTP build sidecars |
| `mcp` | array | no | — | inline MCP servers |
| `mcp_file` | string | no | — | path to a JSON file with MCP servers (`~` expanded) |
| `mcp_override` | boolean | no | `false` | mount the generated MCP config even when the workspace already has one |
| `git` | object | no | — | token and author identity |
| `dind` | boolean | no | `false` | bind-mounts the host Docker socket |
| `secrets` | map of string → string | no | — | variable name → script path, `$ENV_REF`, or literal |
| `claude_settings` | string | no | — | path to a Claude Code `settings.json` (`~` expanded) |
| `name` | string | no | filename | injected automatically on load |
| `description` | string | no | — | free text |
| `enabled` | boolean | no | — | stored and preserved, but **not enforced** by any command |

## Full example

```yaml
image: giglabo/claude-heretic:latest
runner: docker
agent_type: claude
provider: anthropic
description: "Claude agent with full tooling"

# Secrets — resolved before interpolation, never stored in YAML
secrets:
  ANTHROPIC_API_KEY: "~/.heretic/get-anthropic-key.sh"
  ZAI_API_KEY: "~/.heretic/get-secret.sh zai"

volumes:
  - source: "${CWD}"
    target: /workspace
    readonly: false
  - source: "${CWD}/.env"
    target: /workspace/.env
    readonly: true

env:
  ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY}"     # from secrets above
  NODE_ENV: development

workdir: /workspace
command: ["--profile", "default"]
interactive: true
tty: true

extra:
  network: host
  ports: ["3000:3000", "5173:5173"]
  capabilities: [SYS_PTRACE]
  privileged: false
  user: "1000:1000"
  run_as_root: false
  hostname: heretic-agent
  memory: "4g"
  cpus: "2.0"
  shm_size: "2g"
  labels:
    team: platform

tool_backends:
  sidecars:
    - runtime: node
      image: heretic-builder-node:latest
      cache_volumes: ["heretic-npm-cache:/home/builder/.npm"]
  workspace_target: /workspace
  ready_timeout: 60

mcp_file: ~/shared/mcp-servers.json
mcp:
  - name: filesystem
    command: npx
    args: ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
  - name: github
    command: npx
    args: ["-y", "@modelcontextprotocol/server-github"]
    env:
      GITHUB_TOKEN: "${GH_TOKEN}"
mcp_override: false

git:
  token: "${GH_TOKEN}"
  author_name: "Heretic Agent"
  author_email: "agent@example.com"

dind: false
claude_settings: ~/.heretic/claude-settings.json
```

## `volumes[]`

```yaml
volumes:
  - source: ${CWD}          # host path; ${VAR} allowed; must be absolute after interpolation
    target: /workspace      # container path
    readonly: false         # optional → mounted :ro
```

Local overrides **replace the whole array** — adding one mount means re-listing all of them.

## `extra`

| Key | Type | docker runner | compose runner |
|-----|------|---------------|----------------|
| `network` | string | `NetworkMode` | `network_mode` |
| `ports` | array of `"host:container"` | port bindings; `"8080"` maps the same port both sides | `ports:` verbatim |
| `capabilities` | array | `CapAdd` | `cap_add` |
| `privileged` | boolean | `Privileged` | `privileged` |
| `user` | `"uid:gid"` | `User` | `user` |
| `run_as_root` | boolean | forces `User: root`, sets `HERETIC_RUN_AS_ROOT=1`, bind-mounts the embedded entrypoint | same, via `user: root` plus the volume |
| `hostname` | string | `Hostname` | `hostname` |
| `memory` | `4g` / `512m` / `1024k` / bytes | parsed to bytes → `Memory` | `deploy.resources.limits.memory` |
| `cpus` | `"2.0"` | `NanoCpus` | `deploy.resources.limits.cpus` |
| `shm_size` | string | parsed to bytes → `ShmSize` | `shm_size` |
| `labels` | map | merged **after** heretic's own labels, so it can override them | same |

Invalid values fail fast: `Invalid memory format: <v>`, `Invalid CPU value: <v>`, `Invalid port format`. `extra.ports` and `extra.capabilities` are replaced (not merged) by an override.

### Running as root

```yaml
extra:
  run_as_root: true
```

Or per run: `heretic-cli run claude --root`. The container runs as root while `HOME` stays `/home/agent`, so every bind-mounted configuration file still resolves. See [run](https://giglabo.com/heretic/docs/heretic-cli/commands/run#root-mode).

## `agent_type`

| Type | Generated MCP config is mounted to | Session directory becomes |
|------|-----------------------------------|---------------------------|
| `claude` (default) | `/workspace/.mcp.json` | `~/.claude` (both `/home/agent` and `/root`) |
| `aider` | `/workspace/.mcp.json` | `~/.claude` |
| `generic` | `/workspace/.mcp.json` | `~/.claude` |
| `copilot-cli` | `/root/.copilot/mcp-config.json` **and** `/home/agent/.copilot/mcp-config.json` | `~/.copilot` |

Copilot entries additionally get `type: "stdio"` (when missing) and `tools: ["*"]`. Details on [MCP Servers](https://giglabo.com/heretic/docs/heretic-cli/configuration/mcp-servers).

## `ssh`

```yaml
ssh:
  host: dev-server.example.com    # required, non-empty
  port: 22                        # optional, 1–65535
  user: agent                     # optional, default "agent"
  key_path: "${HOME}/.ssh/id_rsa" # optional, must be ABSOLUTE; mounted read-only
  host_cwd: /home/agent/workspace # optional working directory on the remote host
```

Exports `SSH_HOST`, `SSH_PORT`, `SSH_USER`, `SSH_KEY_PATH`, `SSH_HOST_CWD`; the image entrypoint then generates wrappers for missing toolchain commands. Merged key by key by an override. See [SSH Host Access](https://giglabo.com/heretic/docs/heretic-cli/configuration/ssh).

## `tool_backends`

```yaml
tool_backends:
  sidecars:
    - runtime: node               # node | python | java | go | rust only
      image: heretic-builder-node:latest
      port: 8080                  # optional, default 8080
      command: ["exec-server", "-port", "8080", "-cwd", "/workspace"]
      env: { NPM_CONFIG_FUND: "false" }
      env_passthrough: ["NPM_TOKEN"]
      cache_volumes: ["node-cache:/home/builder/.npm"]
  workspace_target: /workspace    # default; MUST match one volume target
  ready_timeout: 60               # seconds, ≥ 1
  run_as_caller_uid: true         # default true
```

Scalar keys merge on override; the `sidecars` array is replaced. Full reference: [Sidecars](https://giglabo.com/heretic/docs/heretic-cli/configuration/sidecars).

## `git`

| Field | Environment variable |
|-------|----------------------|
| `token` | `GH_TOKEN`, `GITHUB_TOKEN` (overriding the global settings token) |
| `author_name` | `GIT_AUTHOR_NAME` |
| `author_email` | `GIT_AUTHOR_EMAIL` |

An empty `token` is a validation error: `git.token must be non-empty when specified`.

## GitHub and Copilot token injection

| Variable | Source | Injected for |
|----------|--------|--------------|
| `GH_TOKEN`, `GITHUB_TOKEN` | `settings.yaml` → `github.token`, or `git.token` | all agents |
| `GH_COPILOT_TOKEN`, `GITHUB_COPILOT_TOKEN` | `github.copilot_token`, falling back to `github.token` | `provider: copilot` only |

## `secrets`

```yaml
secrets:
  CLAUDE_API_KEY: ~/.heretic/get-claude-key.sh   # script → stdout (trimmed)
  ZAI_KEY: $ZAI_TOKEN                            # environment reference
  PLAIN: sk-literal-value                        # literal
```

Resolved secrets join the interpolation context, so `env` can reference them as `${CLAUDE_API_KEY}`. With the docker runner a secret that never appears in `env` is **not** exported into the container; the compose and custom runners pass the whole map into the service environment. See [Secrets](https://giglabo.com/heretic/docs/heretic-cli/configuration/secrets).

## Environment transformations at run time

| Input | Result inside the container |
|-------|----------------------------|
| `ANTHROPIC_API_KEY: X` | `ANTHROPIC_AUTH_TOKEN=X` **and** `ANTHROPIC_AUTH_KEY=X` (the original name is dropped by the docker runner) |
| a variable explicitly set to `""` in `env` | preserved — this is how OAuth mode disables API fallback |
| any other variable resolving to `""` | **removed**, so it cannot shadow the image default |
| `extra.run_as_root: true` | `HERETIC_RUN_AS_ROOT=1` |
| sidecars configured | `BUILD_SIDECARS`, `SIDECAR_ENV_PASSTHROUGH` |
| `ssh` configured | `SSH_HOST`, `SSH_PORT`, `SSH_USER`, `SSH_KEY_PATH`, `SSH_HOST_CWD` |

The complete list is on the [Environment Variables](https://giglabo.com/heretic/docs/heretic-cli/reference/environment-variables) page.

## Automatic mounts

Beyond your `volumes`, the docker and compose runners always add:

```
.heretic/temp/<session>            → /home/agent/.claude and /root/.claude
                                     (or the .copilot pair for agent_type: copilot-cli)
.heretic/temp/<session>/.claude.json → /home/agent/.claude.json and /root/.claude.json
                                       (non-copilot agents only)
.heretic/temp/<session>/entrypoint.sh → /entrypoint.sh:ro          (run_as_root only)
<host docker socket>               → /var/run/docker.sock          (dind: true)
<ssh.key_path>                     → /home/agent/.ssh/id_rsa:ro    (when set)
generated .mcp.json                → see the agent_type table above
```

`claude_settings` is **not** mounted directly: it is merged with `.heretic/cli/claude-settings.json` and written as the session's `settings.json`, which the container sees as `~/.claude/settings.json`. A missing global file logs a warning and skips the merge.

## Docker-in-Docker

With `dind: true` the host Docker socket is bind-mounted to `/var/run/docker.sock`. The host path is platform-specific:

| Platform | Host socket |
|----------|-------------|
| Linux | `/var/run/docker.sock` |
| macOS | `~/.docker/run/docker.sock`, falling back to `/var/run/docker.sock` |
| Windows | `//./pipe/docker_engine` |

> **Warning: DinD grants host-level control**
>
> A container with the Docker socket can start privileged containers on the host. Prefer [build sidecars](https://giglabo.com/heretic/docs/heretic-cli/configuration/sidecars) when all you need is a toolchain.

## Container labels

| Label | Value |
|-------|-------|
| `heretic.managed` | `true` |
| `heretic.agent` | profile name |
| `heretic.project` | absolute project directory |
| `heretic.session` | session name |
| `heretic.network` | per-run sidecar network (when sidecars are used) |

`heretic-cli ps`, `stop` and `attach` rely entirely on these labels.

## Onboarding seeding

For non-Copilot agents the runner seeds `.claude.json` with `{ "hasCompletedOnboarding": true }` and mounts it at both `/home/agent/.claude.json` and `/root/.claude.json`, which skips Claude Code's interactive login screen. Copilot agents get the `~/.copilot` mount instead and no seed.

## Runner selection

| Runner | Use when | What it manages |
|--------|----------|-----------------|
| `docker` (default) | a single container | one container through the Docker API; every profile field applies |
| `compose` | the agent plus extra services (database, cache, local MCP server) | a generated compose file with an `agent` service plus your `compose.services` |
| `custom` | you want full control | your `.heretic/cli/compose.yaml`, verbatim; `extra` and `compose` in the profile are ignored, and build sidecars are rejected |

```yaml
runner: compose
compose:
  services:
    redis:
      image: redis:7-alpine
      ports: ["6379:6379"]
```

The agent reaches Redis at `redis:6379` on the shared compose network. Full comparison: [Runners](https://giglabo.com/heretic/docs/heretic-cli/configuration/runners).

## Ready-to-use profiles

```yaml
# docker, Claude, workspace mount, key from a script
image: giglabo/claude-heretic:latest
runner: docker
provider: anthropic
agent_type: claude
workdir: /workspace
volumes: [{ source: "${CWD}", target: /workspace }]
env: { ANTHROPIC_API_KEY: "${CLAUDE_API_KEY}" }
secrets: { CLAUDE_API_KEY: ~/.heretic/get-claude-key.sh }
claude_settings: ~/.heretic/claude-settings.json
```

```yaml
# compose, extra database service, node build sidecar
image: giglabo/claude-heretic:latest
runner: compose
workdir: /workspace
volumes: [{ source: "${CWD}", target: /workspace }]
compose:
  services:
    db: { image: postgres:16, environment: { POSTGRES_PASSWORD: dev } }
  volumes: { pgdata: null }
tool_backends:
  sidecars: [{ runtime: node, image: heretic-builder-node:latest }]
```

```yaml
# custom runner — uses .heretic/cli/compose.yaml verbatim
image: unused-but-required:latest
runner: custom
```

## Next Steps

- [Local Overrides](https://giglabo.com/heretic/docs/heretic-cli/configuration/local-overrides) — per-project changes
- [Merge Rules](https://giglabo.com/heretic/docs/heretic-cli/reference/merge-rules) — which layer wins, field by field
- [Validation](https://giglabo.com/heretic/docs/heretic-cli/reference/validation) — every rule and error message
- [Environment Variables](https://giglabo.com/heretic/docs/heretic-cli/reference/environment-variables) — everything injected and consumed

## Related

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