Skip to content

fix(install): union-merge hook matcher arrays in merge_settings() #556

@bakeb7j0

Description

@bakeb7j0

Problem

merge_settings() in install (line 222-310) only adds new event keys from the template to a user's ~/.claude/settings.json. It cannot add new matcher entries inside an event array that already exists locally. This is a silent gap — ./install --check reports the event as "present" and shows no drift.

Concrete example (the trigger)

In #555, config/settings.template.json adds two changes to SessionStart:

  1. The pre-existing matcher: "*" entry (crystallizer session-start)
  2. A NEW matcher: "compact" entry (nerf session-start-compact hook)

For a user upgrading from a prior install:

  • SessionStart already exists in their local settings → the entire template SessionStart block is dropped during merge → the new compact matcher is never added → the post-compact statusline repaint hook never fires.
  • PreCompact is genuinely a new event key → merged correctly.

The user gets a half-wired fix and has no way to know without manually diff'ing template vs local.

Root cause

install line 258:

($tpl_hooks | map(select(.key | IN($local_hook_keys[]) | not))) as $new_hooks

This is a "missing event keys only" filter. The same file already does union-merge correctly for permissions.allow (line 261) — this issue is to extend that pattern to hook event arrays.

Changes

Modify merge_settings() in install to:

  • For each event key in the template AND local: union the matcher arrays. Add any template matcher entry whose .matcher value is not in the local array. Leave existing local entries untouched (preserves user customizations).
  • Existing semantics for event keys present in template only / local only are preserved.
  • Update the report loop to surface "added matcher X to event Y" messages.

Tests

  • Add a test fixture pair: a template with a 2-matcher SessionStart array, a local with a 1-matcher SessionStart array, run merge_settings, assert the second matcher appears in output.
  • Add the inverse: a template entry with matcher: "*" matching an existing local * entry but with a different command — assert the local entry is preserved unchanged (no overwrite).
  • ./install --check should report "matcher X added to event Y" for actual gaps.

Acceptance Criteria

  • After ./install runs against a settings.json that has SessionStart: [{matcher: "*", ...}], the resulting file has BOTH * and any new template matchers (e.g. compact).
  • User customizations on existing matchers are NOT overwritten — if local has SessionStart: [{matcher: "*", hooks: [{command: "user-script"}]}] and template has SessionStart: [{matcher: "*", hooks: [{command: "default-script"}]}], the local entry survives.
  • ./install --check reports the actual gap when a matcher is missing locally — currently it reports the event as "already present" even when matcher coverage is incomplete.
  • Test added under tests/scripts/ (or wherever install tests live) covering all three cases above.

Non-goals

Dependencies

  • None. Self-contained change to install.

Related

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions