Local Overrides
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.
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
# .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) |
volumes is all-or-nothing
Adding one mount locally means re-listing every mount, including the workspace:
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.
Recipes
A project-only secret, without touching the global profile
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
extends: claude-zai
env:
ANTHROPIC_MODEL: glm-4.5-air
Add a build sidecar for this repository only
extends: claude
tool_backends:
sidecars:
- runtime: node
image: heretic-builder-node:latest
Swap the image for a locally built one
extends: claude
image: heretic-agent:dev
MCP behaviour
mcp_fileservers form the base; inlinemcpentries override them by name and append new ones.- Both
mcpandmcp_fileare 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.jsonfor Copilot), heretic does not mount its generated one — setmcp_override: trueto force it. A zero-byte or unparseable file counts as absent.
See MCP Servers.
Traps
- A file that exists must parse to an object. The freshly generated template is all comments, which YAML reads as
null—runandlocal-validatethen fail withInvalid config: expected an object. Uncomment something, or delete the file. local-validateprints 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: 8080is a number in YAML and fails validation withEnvironment 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 — the scaffolding command and
local-validate - Merge Rules — field-by-field precedence
- Secrets — value modes and failure messages