Agent Profiles
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
image: giglabo/claude-heretic:latest
runner: docker
Everything else has a default. See Docker Images for image options and heretic-cli image build 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
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: "[email protected]"
dind: false
claude_settings: ~/.heretic/claude-settings.json
volumes[]
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
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.
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.
ssh
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.
tool_backends
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.
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
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.
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 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 |
DinD grants host-level control
A container with the Docker socket can start privileged containers on the host. Prefer build 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 |
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.
Ready-to-use profiles
# 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
# 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 }]
# custom runner — uses .heretic/cli/compose.yaml verbatim
image: unused-but-required:latest
runner: custom
Next Steps
- Local Overrides — per-project changes
- Merge Rules — which layer wins, field by field
- Validation — every rule and error message
- Environment Variables — everything injected and consumed