---
title: Local Overrides
description: "Per-project configuration — per-profile override files, the legacy agent.yaml fallback, what merges and what is replaced, recipes, and the traps to avoid"
canonical: https://giglabo.com/heretic/docs/heretic-cli/configuration/local-overrides
locale: en
---

# Local Overrides

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

Per-project configuration — per-profile override files, the legacy agent.yaml fallback, what merges and what is replaced, recipes, and the traps to avoid

A local override is a YAML file in `<project>/.heretic/cli/` that customises a global profile for this project only. It uses the **same schema** as a profile, plus the `extends` key.

```bash
heretic-cli local-init claude      # scaffold .heretic/cli/claude.yaml
heretic-cli local-validate claude  # resolve and check it
```

## File resolution

| File | When it applies |
|------|-----------------|
| `.heretic/cli/<profile>.yaml` | **preferred** — `extends` is inferred from the filename |
| `.heretic/cli/agent.yaml` | legacy fallback — applies only when its `extends:` names the profile you run |
| `.heretic/cli/compose.yaml` | reserved — used verbatim by `runner: custom` |
| `.heretic/cli/claude-settings.json` | reserved — merged over the profile's Claude settings |

A per-profile file always wins over the legacy `agent.yaml`. Declaring a mismatched `extends` is fatal: `Local config extends 'a' but loading profile 'b'`.

## Example

```yaml
# .heretic/cli/claude.yaml — local override for the "claude" profile
extends: claude          # optional here: inferred from the filename

# merged with the profile's secrets, key by key
secrets:
  PROJECT_API_KEY: "~/.heretic/get-project-key.sh"

# merged with the profile's env, key by key
env:
  PROJECT_NAME: my-app
  DEBUG: "app:*"

# load MCP servers from the project's VS Code config
mcp_file: ${CWD}/.vscode/mcp.json

# inline servers are merged over the mcp_file ones by name
mcp:
  - name: project-docs
    command: npx
    args: ["-y", "@modelcontextprotocol/server-filesystem", "/workspace/docs"]

mcp_override: true       # mount the generated config even if the workspace has .mcp.json

git:
  author_name: "Project Bot"

extra:
  ports: ["5173:5173"]   # REPLACES any ports from the profile

dind: true
```

## What merges and what is replaced

| Behaviour | Fields |
|-----------|--------|
| **Replaced entirely** | `image`, `runner`, `agent_type`, `provider`, `volumes`, `workdir`, `command`, `interactive`, `tty`, `mcp`, `mcp_file`, `mcp_override`, `dind`, `claude_settings` |
| **Merged key by key** | `env`, `extra`, `ssh`, `git`, `secrets`, `extra.labels` |
| **Replaced inside `extra`** | `extra.ports`, `extra.capabilities` |
| **Deep merged** | `compose` (objects recurse; arrays and primitives replace) |
| **Scalars merge, list replaces** | `tool_backends` (`sidecars` is replaced) |

> **Warning: volumes is all-or-nothing**
>
> Adding one mount locally means re-listing every mount, including the workspace:
>
> ```yaml
> extends: claude
> volumes:
> - { source: "${CWD}", target: /workspace }
> - { source: "${HOME}/.gitconfig", target: /home/agent/.gitconfig, readonly: true }
> ```

The full table with examples is on [Merge Rules](https://giglabo.com/heretic/docs/heretic-cli/reference/merge-rules).

## Recipes

**A project-only secret, without touching the global profile**

```yaml
extends: claude
secrets:
  PROJECT_TOKEN: $CI_TOKEN      # from the shell environment; fails loudly when unset
env:
  PROJECT_TOKEN: ${PROJECT_TOKEN}
```

**Override a single model for this repository**

```yaml
extends: claude-zai
env:
  ANTHROPIC_MODEL: glm-4.5-air
```

**Add a build sidecar for this repository only**

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

**Swap the image for a locally built one**

```yaml
extends: claude
image: heretic-agent:dev
```

## MCP behaviour

- `mcp_file` servers form the base; inline `mcp` entries override them **by name** and append new ones.
- Both `mcp` and `mcp_file` are *replaced* by the override layer, never merged with the profile's array.
- If the project already contains a usable MCP config (`.mcp.json`, or `.copilot/mcp-config.json` for Copilot), heretic does not mount its generated one — set `mcp_override: true` to force it. A zero-byte or unparseable file counts as absent.

See [MCP Servers](https://giglabo.com/heretic/docs/heretic-cli/configuration/mcp-servers).

## Traps

- **A file that exists must parse to an object.** The freshly generated template is all comments, which YAML reads as `null` — `run` and `local-validate` then fail with `Invalid config: expected an object`. Uncomment something, or delete the file.
- **`local-validate` prints resolved secrets in plaintext.** Keep the output out of tickets and CI logs.
- **`.heretic/cli/` belongs in `.gitignore`** — it contains absolute host paths and secret references.
- Environment values must be strings: `PORT: 8080` is a number in YAML and fails validation with `Environment variable 'PORT' must be a string (got number)`. Quote it.
- There is no `local-clean`; reset an agent's state by deleting `.heretic/temp/<session>/`.

## Next Steps

- [local-init](https://giglabo.com/heretic/docs/heretic-cli/commands/local-init) — the scaffolding command and `local-validate`
- [Merge Rules](https://giglabo.com/heretic/docs/heretic-cli/reference/merge-rules) — field-by-field precedence
- [Secrets](https://giglabo.com/heretic/docs/heretic-cli/configuration/secrets) — value modes and failure messages

## Related

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