Patches
If you want the same knob on several profiles — a shared system prompt, a team MCP file — don't repeat it per profile: put it in a root-level patches entry. A patch is a partial profile shape that merges onto whichever profile gets picked at resolve time.
This is the single mechanism for profile-shared knobs — there is deliberately no root-level system_prompt / mcps / mcp field.
Writing a patch
patches:
# Unscoped — applies to every profile.
- system_prompt:
- file: ~/.config/hyprpilot/prompts/base.md
# Scoped — only profiles whose id matches the glob.
- $match:
profile: work/*
mcps:
- file: ~/.config/hyprpilot/mcps/work.jsonAnything you can write under a profiles entry you can write in a patch — the patch body is the same partial-profile shape.
Shape
Each patches entry is a partial profile shape — any profile field is valid — plus one optional control sibling:
| Field | Type | Default | What it does |
|---|---|---|---|
$match | object (optional) | unset | Filters which profiles the patch applies to; stripped before the merge. |
| (rest) | partial profile | — | Fields folded onto the picked profile with the strategic-merge engine. |
$match
| Field | Type | Default | What it does |
|---|---|---|---|
profile | glob (optional) | unset | Profile-id glob (crosses /, so work/* matches work/claude/opus). Unset = every profile. |
Patches fold left-to-right in declaration order; a later patch wins on field collision.
Additive across layers
patches accumulates across config layers instead of overwriting: the compiled defaults' patches come first, then your global config's, then the named config-layer profile's — each layer appends to the list, and the whole accumulated list folds onto the picked profile in that order.
That means a config-layer profile can add a work-only MCP patch without wiping the seeded default patch or your global ones. It also means you cannot delete an earlier layer's patch by redeclaring the list — to neutralize an inherited patch, add a later patch that overrides the same fields, using the merge directives to wipe rather than merge:
# In a later layer: undo an inherited patch's extra prompts for `scratch`.
patches:
- $match:
profile: scratch
system_prompt:
- $patch: replace # sentinel: replace the whole array with the rest (nothing)Merge semantics
Patches fold with a strategic-merge engine — the same one --with-config uses:
| Directive | Where | Effect |
|---|---|---|
| (none) | objects | Recursive field merge; scalar leaves overwrite (later wins). |
| (none) | keyed arrays | Merge by id; new ids append. |
| (none) | primitive arrays | Append + de-duplicate. |
$patch: replace (object key) | objects / maps | field: { $patch: replace, … } drops the base value and takes the patch's. |
- $patch: replace (sentinel) | arrays | A { $patch: replace } first element replaces the whole array with the remaining elements. |
$patch: delete (keyed entry) | keyed arrays | { id: …, $patch: delete } removes the entry with that id. |
$deleteFromPrimitiveList/<field> | primitive arrays | Remove the listed entries from the base array. |
$patch: replace also works on the profile side — a profile can shield a field from patch overlays:
profiles:
- id: clean
agent: claude-code
env:
$patch: replace # ignore any env a patch would overlayThe default patch
The compiled defaults seed one unscoped patch that points the in-tree hyprpilot MCP server at the XDG skills directory — this is why skills work out of the box:
patches:
- mcp:
skills:
- dir: ~/.config/hyprpilot/skillsThe seed carries only the skills root. The other mcp knobs — enabled: true, autoAcceptTools: ['*'], autoRejectTools: [] — are the built-in defaults, backfilled per leaf at resolve time rather than spelled out in the seed. Because patches accumulate, your own patches entries land after this seed (later wins on field collision) — or override the mcp field per-profile.
Where patches sit in resolution
Every launch resolves the effective profile through one path:
- Pick the base profile — the positional
[PROFILE]id first, thenprofile.default. Errors when neither names a realprofilesentry. - Fold each accumulated
patchesentry (filtered by its$match.profileglob) in declaration order. - Fold each
--with-configoverlay in declaration order. - Deserialize the merged result back into a profile and re-validate.
The resolved profile is the single source of truth for its agent and model — there is no per-launch --agent / --model override. To swap either for one launch, add an --with-config overlay (e.g. --with-config '@{"agent":"codex"}').