---
title: Configuration
description: "The three-layer configuration model — global profiles, per-project overrides, CLI flags, variable interpolation, and every file heretic-cli reads or writes"
canonical: https://giglabo.com/heretic/docs/heretic-cli/configuration
locale: en
---

# Configuration

> Markdown twin of https://giglabo.com/heretic/docs/heretic-cli/configuration
> 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

The three-layer configuration model — global profiles, per-project overrides, CLI flags, variable interpolation, and every file heretic-cli reads or writes

## Three layers

Every run resolves configuration by merging three layers — later wins:

```
1. Global profile     ~/.heretic/agents/<name>.yaml
2. Local override     <project>/.heretic/cli/<name>.yaml     (or the legacy agent.yaml)
        ↓
3. CLI flags          --mcp, --root, --sidecar / --builder-image / --disable-sidecars
        ↓
   ${VAR} interpolation over the merged result
        ↓
   mcp_file merge → defaults → validation
```

Merging is **per field**, not a blanket deep merge: `env`, `extra`, `ssh`, `git` and `secrets` merge key by key, while `volumes`, `mcp`, `extra.ports` and `extra.capabilities` are replaced wholesale. The exact table is on the [Merge Rules](https://giglabo.com/heretic/docs/heretic-cli/reference/merge-rules) page.

A local override must declare which profile it belongs to. A mismatch is fatal: `Local config extends 'a' but loading profile 'b'`.

## Variable interpolation

`${VAR}` placeholders are expanded in **every string** of the merged configuration, recursively through objects and arrays, *after* the merge.

| Variable | Value |
|----------|-------|
| `${CWD}` | the project directory (where you ran the command) |
| `${HOME}` | your home directory |
| `${ANY_ENV_VAR}` | any variable from the host environment |
| `${MY_SECRET}` | any key from `secrets:`, because secrets are resolved first |

- An unknown variable logs `Variable "X" is undefined, resolving to empty string` and expands to `""` — and empty values are then stripped from the container environment unless the key was explicitly set to `""`.
- Escape with a double dollar: `$${LITERAL}` renders as `${LITERAL}`.
- Only the braced form is recognised in configuration values. A bare `$VAR` is left as-is (it *is* recognised inside `secrets:` values, which take a different code path).

## Global files

```
~/.heretic/
├── settings.yaml                    github.token / github.copilot_token (script paths or raw values)
├── agents/
│   └── <name>.yaml                  one agent profile per file — the profile name is the filename
├── get-<name>-key.sh|.cmd           per-agent secret script created by `init` (mode 0755 on Unix)
├── get-github-token-key.sh|.cmd     GitHub token script
├── get-copilot-token-key.sh|.cmd    Copilot token script
└── <name>-settings.json             per-agent Claude Code settings
```

## Project files

```
<project>/.heretic/
├── cli/
│   ├── <profile>.yaml               per-profile override — PREFERRED
│   ├── agent.yaml                   legacy single override; applies only when its `extends:` matches
│   ├── compose.yaml                 RESERVED — used verbatim by runner: custom
│   └── claude-settings.json         RESERVED — merged over the global settings at run time
└── temp/<session>/                  generated per run (mode 0777), mounted into the container
    ├── .mcp.json                    generated MCP configuration
    ├── settings.json                merged Claude settings → ~/.claude/settings.json
    ├── .claude.json                 onboarding seed {"hasCompletedOnboarding": true}
    ├── compose.yaml                 generated compose file (compose runner)
    └── entrypoint.sh                embedded entrypoint (root mode only)
```

> **Note: Reserved filenames**
>
> Inside `.heretic/cli/` every `*.yaml` / `*.yml` file is treated as a profile override **except** `compose.yaml` and `claude-settings.json`. Add `.heretic/cli/` to `.gitignore` — it holds machine-specific paths and secret references.

The session directory is regenerated on every run and is where the agent's `~/.claude` (or `~/.copilot`) state lives, so it survives across runs of the same session. Deleting `.heretic/temp/<session>/` resets that agent's history.

## Inspecting a resolved configuration

```bash
heretic-cli agents show <name>              # the raw profile file
heretic-cli agents show <name> --resolved   # after merge, interpolation and secret resolution
heretic-cli local-validate <name>           # the complete resolved object (secrets in plaintext)
```

## Detailed topics

- [Agent Profiles](https://giglabo.com/heretic/docs/heretic-cli/configuration/agent-profiles) — every profile field
- [Local Overrides](https://giglabo.com/heretic/docs/heretic-cli/configuration/local-overrides) — per-project configuration
- [Secrets](https://giglabo.com/heretic/docs/heretic-cli/configuration/secrets) — the three secret value modes
- [MCP Servers](https://giglabo.com/heretic/docs/heretic-cli/configuration/mcp-servers) — formats, transports, mount paths
- [Runners](https://giglabo.com/heretic/docs/heretic-cli/configuration/runners) — docker, compose, custom
- [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) — running tools on a remote host
- [Sessions](https://giglabo.com/heretic/docs/heretic-cli/configuration/sessions) — multiple isolated instances
- [Docker Images](https://giglabo.com/heretic/docs/heretic-cli/configuration/docker-images) — choosing and building images

## Related

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