Config Overview
Everything hyprpilot does is driven by one layered config: which vendors it can launch, which session profiles exist, which MCP servers and skills a launch carries, and how the launch chrome behaves. This section is the whole configuration surface — one page per root section, each with the narrative and the field-by-field reference together.
Formats
Hyprpilot reads YAML, TOML, or JSON — the same shape in any of them. The docs write YAML throughout (the recommended format, and the most readable for nested profiles); drop a config.toml or config.json instead if you prefer, the field names are identical.
The global config lives at ~/.config/hyprpilot/config.{toml,json,yaml,yml}, searched across the four extensions in priority order:
.toml → .json → .yaml → .ymlIf two files with different extensions coexist at the same layer (say config.toml and config.yaml), hyprpilot errors at load rather than silently picking one. --config <path> infers the format from the extension.
A complete example
# ~/.config/hyprpilot/config.yaml
profile:
default: engineer
profiles:
- id: engineer
agent: claude-code
model: claude-sonnet-4-5
cwd: ~/code/my-project
system_prompt:
- file: ~/.config/hyprpilot/prompts/engineer.md
mcps:
- file: ~/.claude.json
patches:
- system_prompt:
- file: ~/.config/hyprpilot/prompts/base.md
multiplexer:
set_title: true
logging:
level: infoRoot sections
| Section | Shape | Purpose |
|---|---|---|
agents | list, keyed by id | Vendor CLI registry. See Agents. |
profile | singleton | Picks the default session profile. See Profiles. |
profiles | list, keyed by id | Session presets — at least one is required. See Profiles. |
mcp / mcps | block / list | Skills channel + MCP catalogue. See MCP. |
patches | list | Partial-profile overlays, additive across layers. See Patches. |
multiplexer | singleton | tmux/zellij title rename. See Multiplexer. |
logging | singleton | Tracing filter level. See Logging. |
There is no root-level system_prompt / mcps / mcp / cwd field — those are per-profile, or shared via patches.
Layers
Config resolves in layers — compiled defaults → global config → named config-layer profile → patches / --with-config — with later layers overriding earlier ones per field (and patches accumulating across layers). Layering covers discovery, the merge rules, and validation.
Defaults quoted in the reference tables come from the compiled src/config/defaults.toml, the single source of truth (the binary embeds TOML internally — your own config can be any of the three formats):
The compiled defaults, verbatim
# `[logging]` is intentionally NOT seeded. Leaving `logging.level`
# unset lets the code fallback in `logging::init` — `error` — own the
# default, keeping a fresh run quiet (errors only) unless a level is
# explicitly requested via `--log-level`, `RUST_LOG`, or `[logging]
# level`. Seeding a level here would re-nullify the scoped `[logging]
# level` filter (K-750), so the CODE fallback owns the default. Set
# `[logging] level` in your own config to raise verbosity.
# `[multiplexer]` — best-effort tmux/zellij window/tab rename right
# before `exec()`-ing into the vendor CLI. Titles as
# `hyprpilot@<resolved-cwd-basename>` so the captain can tell agent
# panes apart. No-op outside tmux/zellij regardless of this flag.
[multiplexer]
set_title = true
# `[[patches]]` — root-level profile patches. Each patch is a
# partial `ProfileConfig` shape that gets folded onto whichever
# profile the captain picked. An optional `$match.profile` glob
# filters which profiles a patch applies to — unset means "applies
# to every profile". Same strategic-merge engine the `--with-config`
# flag uses; `$patch: replace` / `$deleteFromPrimitiveList/<field>`
# directives compose.
#
# **Additive across config layers**: a user config layer's
# `[[patches]]` EXTENDS this list rather than replacing it, so the
# seed below always survives. Captains add more `[[patches]]` entries
# (later wins on field collision) or override the `mcp` field
# per-profile; to wipe an earlier patch's field, fold a later patch
# with `$patch: replace` inside the field body.
#
# Default seed: one unscoped patch carrying the skills directory, the
# name each in-tree server is injected under, and the harness's
# tunable ceilings.
#
# `[mcp] enabled`, `autoAcceptTools` and `autoRejectTools` are NOT here:
# their accessors `.expect()` a value, so they must come from the typed
# `McpConfig::default()` the resolver backfills per-leaf, which a
# programmatic `Config` carrying no patches also gets. The
# `[mcp.harness]` block below is not backfilled that way (it is nested,
# and the typed default leaves it `None`), so its numbers live here —
# the file a captain actually edits — with the Rust fallbacks in
# `config/mcp.rs` covering only the no-patches case. A paired test pins
# the two equal, so they cannot drift apart.
[[patches]]
[patches.mcp]
# `maxDepth` — how many levels of harness delegation are allowed. `1`
# means you may spawn delegates and they may not. Read in one place: the
# `[mcp.harness]` block a gate is deciding on. It answers both "does a
# session at depth d get a harness injected" and "may a sidecar at depth
# d spawn" with the same `d < maxDepth`. `0` denies both everywhere.
#
# Raising it is a resource decision, not a security one — a session
# ceiling bounds one sidecar's own table, so N delegates each running N
# sessions fans out past anything the lead can see.
#
# `maxSessions` — finished sessions retained per sidecar before the
# oldest are evicted with their transcripts. A running session is never
# evicted and never counts against this. `0` retains everything.
#
# `maxLiveSessions` — sessions allowed to RUN at once before `spawn` is
# refused. `0`, the default, allows any number: how many agents are
# worth running at once belongs to your machine and your work, not to
# this file. Set it on a shared or resource-tight host.
#
# Written snake_case to match the rest of this file. Both spellings
# parse, but patches merge by KEY STRING before anything is typed, so a
# key seeded here must be OVERRIDDEN in the same spelling — `maxDepth`
# in your config against `max_depth` here arrives at serde as two keys
# and fails config load with `duplicate field`.
[patches.mcp.harness]
name = "hyprpilot-harness"
max_depth = 1
max_sessions = 64
max_live_sessions = 0
notify_on_complete = true
# `name` is the key the vendor's MCP catalog is written under, so it is
# also the tool prefix the agent sees (`mcp__hyprpilot-skills__read_skill`)
# and the name RESERVED against a same-named configured server. The
# injector reads this field and nothing else, so renaming a server is a
# config edit rather than a rebuild. The Rust constants in
# `config/mcp.rs` only cover a `Config` carrying no patches; a test pins
# them equal to these.
[patches.mcp.serve]
name = "hyprpilot"
[patches.mcp.skills]
name = "hyprpilot-skills"
[[patches.mcp.skills.dirs]]
dir = "~/.config/hyprpilot/skills"
# `[[agents]]` — vendor registry. `command` / `args` are the NATIVE
# vendor CLI the launcher `exec`s into directly (no ACP bridge). The
# launcher projects the resolved profile's model / effort / mode /
# system-prompt / MCP catalog onto each vendor's native flags at
# spawn time; bare `args = []` launches the vendor's interactive TUI.
[[agents]]
id = "claude-code"
provider = "claude-code"
command = "claude"
args = []
[[agents]]
id = "codex"
provider = "codex"
command = "codex"
args = []
[[agents]]
id = "opencode"
provider = "opencode"
command = "opencode"
args = []
# `[[profiles]]` — captain-supplied. NOT seeded by defaults so a
# fresh install presents a single source of truth for "which profiles
# exist": the captain's on-disk config. Validation rejects an empty
# `[[profiles]]` list at config-load, so fresh installs without any
# profile won't launch — the captain configures at least one before
# spawning. `[profile] default` picks which profile runs when the
# positional `[PROFILE]` argument isn't passed; required when more than
# one profile exists.Validation
Every section validates types and rejects unknown fields at load — typos fail fast with an error naming the offending field path. Cross-field references are checked too: profiles[].agent must reference a real agents[].id, and profile.default must name a real profiles[].id. The profiles list must be non-empty — a fresh install with no profile refuses to launch rather than guessing.