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:
- The pre-existing
matcher: "*" entry (crystallizer session-start)
- 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
Non-goals
Dependencies
- None. Self-contained change to
install.
Related
Problem
merge_settings()ininstall(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 --checkreports the event as "present" and shows no drift.Concrete example (the trigger)
In #555,
config/settings.template.jsonadds two changes toSessionStart:matcher: "*"entry (crystallizer session-start)matcher: "compact"entry (nerf session-start-compact hook)For a user upgrading from a prior install:
SessionStartalready exists in their local settings → the entire templateSessionStartblock is dropped during merge → the newcompactmatcher is never added → the post-compact statusline repaint hook never fires.PreCompactis 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
installline 258: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()ininstallto:.matchervalue is not in the local array. Leave existing local entries untouched (preserves user customizations).Tests
SessionStartarray, a local with a 1-matcherSessionStartarray, runmerge_settings, assert the second matcher appears in output.matcher: "*"matching an existing local*entry but with a different command — assert the local entry is preserved unchanged (no overwrite)../install --checkshould report "matcher X added to event Y" for actual gaps.Acceptance Criteria
./installruns against a settings.json that hasSessionStart: [{matcher: "*", ...}], the resulting file has BOTH*and any new template matchers (e.g.compact).SessionStart: [{matcher: "*", hooks: [{command: "user-script"}]}]and template hasSessionStart: [{matcher: "*", hooks: [{command: "default-script"}]}], the local entry survives../install --checkreports the actual gap when a matcher is missing locally — currently it reports the event as "already present" even when matcher coverage is incomplete.tests/scripts/(or wherever install tests live) covering all three cases above.Non-goals
Dependencies
install.Related
fix/555-stale-statusline-after-compact.