Skip to content

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

yaml
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.json

Anything 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:

FieldTypeDefaultWhat it does
$matchobject (optional)unsetFilters which profiles the patch applies to; stripped before the merge.
(rest)partial profileFields folded onto the picked profile with the strategic-merge engine.

$match

FieldTypeDefaultWhat it does
profileglob (optional)unsetProfile-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:

yaml
# 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:

DirectiveWhereEffect
(none)objectsRecursive field merge; scalar leaves overwrite (later wins).
(none)keyed arraysMerge by id; new ids append.
(none)primitive arraysAppend + de-duplicate.
$patch: replace (object key)objects / mapsfield: { $patch: replace, … } drops the base value and takes the patch's.
- $patch: replace (sentinel)arraysA { $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 arraysRemove the listed entries from the base array.

$patch: replace also works on the profile side — a profile can shield a field from patch overlays:

yaml
profiles:
  - id: clean
    agent: claude-code
    env:
      $patch: replace # ignore any env a patch would overlay

The 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:

yaml
patches:
  - mcp:
      skills:
        - dir: ~/.config/hyprpilot/skills

The 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:

  1. Pick the base profile — the positional [PROFILE] id first, then profile.default. Errors when neither names a real profiles entry.
  2. Fold each accumulated patches entry (filtered by its $match.profile glob) in declaration order.
  3. Fold each --with-config overlay in declaration order.
  4. 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"}').