---
title: Validation
description: "Every validation rule heretic-cli applies — profile shape checks versus post-merge checks, the exact error messages, which commands run which stage, and how to read the results"
canonical: https://giglabo.com/heretic/docs/heretic-cli/reference/validation
locale: en
---

# Validation

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

Every validation rule heretic-cli applies — profile shape checks versus post-merge checks, the exact error messages, which commands run which stage, and how to read the results

Validation happens in **two stages**, and knowing which stage produced an error tells you where to look.

| Stage | Checks | Run by |
|-------|--------|--------|
| **Shape** (a single file) | types, required fields, enum values, port syntax | `agents validate`, and every command that loads a profile |
| **Post-merge** (the resolved config) | absolute paths, cross-field consistency, sidecar wiring, environment value types | `run`, `local-validate`, `doctor` (local configs) |

The shape stage never contacts Docker, never interpolates `${VAR}` and never runs secret scripts. The post-merge stage does resolve secrets, because it validates the values that will actually reach the container.

## Stage 1 — profile shape

```
$ heretic-cli agents validate claude
Validating profile "claude"...
  [pass] YAML syntax
  [pass] Required fields
  [pass] All validation checks passed
```

| Rule | Requirement |
|------|-------------|
| `image` | required, non-empty string |
| `runner` | required; one of `docker`, `compose`, `custom` |
| `agent_type` | one of `claude`, `aider`, `copilot-cli`, `generic` |
| `provider` | one of `anthropic`, `thirdparty`, `copilot` |
| `volumes[i].source` / `.target` | required non-empty strings; `readonly` must be boolean |
| `env` values | strings |
| `extra.ports[i]` | must match `host:container`, optionally with a protocol suffix |
| `ssh.host` | required whenever an `ssh` block is present |
| `mcp[i]` | `name` plus `command` (stdio) or `url` (http) |
| `secrets.*` | non-empty strings |

Exit code is 1 if any check fails. A `[warn]` alone does not fail the command; the only warning implemented is:

```
[warn] compose section present but runner is "docker". Compose config will be ignored.
```

Called without a name, `agents validate` walks every file in `~/.heretic/agents/`. A YAML syntax error prints `[fail] YAML syntax: …`; for a single named profile it stops there, in all-profiles mode it continues with the next file.

> **Note: A profile missing from agents list is usually invalid**
>
> `agents list` skips invalid profiles with a warning rather than failing. If a profile does not show up, run `heretic-cli agents validate`.

## Stage 2 — resolved configuration

Run automatically before every container start, and on demand with `heretic-cli local-validate`. All failures are collected and thrown together:

```
Config validation failed: <error>, <error>, …
```

**Core**

- `Image must be non-empty`
- `Invalid runner type: <x> (must be one of: docker, compose, custom)`
- `Invalid agent type: <x> (must be one of: claude, aider, copilot-cli, generic)`

**Volumes**

- `Volume[i].source must be non-empty`
- `Volume[i].source must be an absolute path: <path>`
- `Volume[i].target must be non-empty`

**Environment**

- `Environment variable '<key>' must be a string (got <type>)` — a bare `PORT: 8080` is a number in YAML; quote it as `"8080"`

**SSH**

- `ssh.host must be non-empty`
- `ssh.port must be between 1 and 65535`
- `ssh.key_path must be an absolute path: <path>` — `~/…` fails here, use `${HOME}/…`

**Build sidecars**

- `tool_backends.sidecars[i].runtime '<x>' is invalid (must be one of: node, python, java, go, rust)`
- `tool_backends.sidecars[i]: duplicate runtime '<x>'`
- `tool_backends.sidecars[i].image must be non-empty`
- `tool_backends.sidecars[i].port must be between 1 and 65535`
- `tool_backends.ready_timeout must be a positive number of seconds`
- `tool_backends: no volume targets '<workspace_target>' — build sidecars need the workspace bind-mounted`

**MCP**

- `mcp[i].name must be non-empty`
- `mcp[i].command must be non-empty`
- `mcp[i].url must be non-empty for http transport`

**Git**

- `git.token must be non-empty when specified`

**Non-fatal warning** — both `tool_backends.sidecars` and `ssh` configured: a single build would then span two filesystems.

## Errors from loading, not validating

| Message | Meaning |
|---------|---------|
| `Profile not found: <name>` | no `~/.heretic/agents/<name>.yaml` |
| `Invalid profile '<name>': <errors>` | stage 1 rejected the file |
| `Failed to load profile '<name>': …` | YAML parse or I/O failure |
| `Local config extends 'a' but loading profile 'b'` | the override belongs to another profile |
| `Failed to load local config from <path>: Invalid config: expected an object` | the file exists but parses to `null` — typically the all-comments `local-init` template |
| `Secret script not found: <path>` | a `secrets:` entry points at a missing script |
| `Secret script failed (<path>): <stderr>` | the script exited non-zero |
| `Secret "X": environment variable "Y" is not set` | a `$VAR` secret reference with nothing behind it |
| `MCP file not found: <path>` | `mcp_file` points nowhere |

## Running validation

```bash
heretic-cli agents validate              # every global profile (stage 1)
heretic-cli agents validate claude       # one profile
heretic-cli local-validate               # every local config (stage 1 + 2 + resolved dump)
heretic-cli local-validate claude        # one local config
heretic-cli agents show claude --resolved # the resolved config, secrets masked
heretic-cli -V run claude                # verbose resolution while starting
```

Remember that `-V/--verbose` is a program-level flag and must precede the subcommand — `run claude --verbose` passes `--verbose` to the container.

> **Warning: doctor does not validate global profiles in 0.1.0**
>
> The profile and image checks in `heretic-cli doctor` report `0 profile(s) validated` regardless of your configuration. Use `agents validate` and `local-validate` — see [doctor](https://giglabo.com/heretic/docs/heretic-cli/commands/doctor).

## Pre-flight checklist before a first run

```bash
heretic-cli agents validate <name>            # shape
heretic-cli agents show <name> --resolved     # interpolation and absolute paths
heretic-cli local-validate <name>             # full resolution incl. secrets and sidecars
docker images | grep <image>                  # is the image available
```

## Next Steps

- [Merge Rules](https://giglabo.com/heretic/docs/heretic-cli/reference/merge-rules) — what the validator sees after merging
- [Troubleshooting](https://giglabo.com/heretic/docs/heretic-cli/reference/troubleshooting) — symptom-first fixes
- [Agent Profiles](https://giglabo.com/heretic/docs/heretic-cli/configuration/agent-profiles) — the schema being validated

## Related

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