---
url: https://hyprpilot.kilic.dev/config.md
---

# {{ $frontmatter.title }}

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:

```txt
.toml → .json → .yaml → .yml
```

If 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

```yaml
# ~/.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: info
```

## Root sections

| Section        | Shape               | Purpose                                                                     |
| -------------- | ------------------- | --------------------------------------------------------------------------- |
| `agents`       | list, keyed by `id` | Vendor CLI registry. See [Agents](./agents).                                |
| `profile`      | singleton           | Picks the default session profile. See [Profiles](./profiles).              |
| `profiles`     | list, keyed by `id` | Session presets — at least one is required. See [Profiles](./profiles).     |
| `mcp` / `mcps` | block / list        | Skills channel + MCP catalogue. See [MCP](./mcp).                           |
| `patches`      | list                | Partial-profile overlays, additive across layers. See [Patches](./patches). |
| `multiplexer`  | singleton           | tmux/zellij title rename. See [Multiplexer](./multiplexer).                 |
| `logging`      | singleton           | Tracing filter level. See [Logging](./logging).                             |

There is **no** root-level `system_prompt` / `mcps` / `mcp` / `cwd` field — those are per-profile, or shared via [`patches`](./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](./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):

::: details The compiled defaults, verbatim

```toml
# `[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"
# `system_prompts` — also serve the launched profile's own
# `system_prompt` files as MCP prompts (and `hyprpilot://prompts/<name>`
# resources), so an edited system prompt can be re-invoked mid-session
# instead of waiting for a relaunch. Snake_case for the duplicate-key
# reason noted on the harness block above.
system_prompts = true

# `[mcp.passthrough]` — forwards each declared tool call as a JSON POST
# to an HTTP endpoint and returns the response verbatim. Off, and no
# tools: an upstream is something you choose, so nothing is seeded that
# points anywhere. Declare `[[mcp.passthrough.tools]]` and set
# `enabled = true` in your own config (or a `$match`ed patch).
#
# `timeout_seconds` — per-request timeout, connect through the last
# byte. Written snake_case for the duplicate-key reason noted above.
[patches.mcp.passthrough]
name = "hyprpilot-passthrough"
timeout_seconds = 300

# `watch` — watch this root and announce edits over MCP, so a change to
# a `SKILL.md` or a declared reference reaches connected clients without
# anyone calling `reload`. Seeded ON here rather than left to the Rust
# accessor for the same reason the harness ceilings are: `[mcp.skills]`
# is nested, so the resolver never backfills its leaves, and the value a
# real launch reads belongs in the file the captain edits.
#
# Turn it off ONLY for a root on a filesystem that cannot deliver events
# — NFS, SSHFS and most FUSE mounts accept the watch and then never fire
# for a remote write, so there is nothing to degrade on and the sidecar
# cannot know. Those roots need `reload`.
[[patches.mcp.skills.dirs]]
dir = "~/.config/hyprpilot/skills"
watch = true

# `[[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 = []

[[agents]]
id = "amp"
provider = "amp"
command = "amp"
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.
