Skip to content

ISSUE-355: Assert hook matcher and command in hookEventExists - #360

Merged
bguidolim merged 1 commit into
mainfrom
bruno/ISSUE-355-hook-matcher-assertion
Jul 30, 2026
Merged

ISSUE-355: Assert hook matcher and command in hookEventExists#360
bguidolim merged 1 commit into
mainfrom
bruno/ISSUE-355-hook-matcher-assertion

Conversation

@bguidolim

Copy link
Copy Markdown
Collaborator

Summary

A hook whose matcher matches nothing installs cleanly, registers, passes doctor, and never fires — the only symptom is an empty log, which reads as "nothing wrong" rather than "never ran." Doctor already derives event/matcher verification for hooks declared as components, but a pack can also ship hooks inside a settings file, and those are invisible to that mechanism: they never enter the artifact record and are excluded from the per-pack settings hash. This adds the one form of verification available for that shape, and fixes two silent behaviors in settings composition that a strict matcher assertion would otherwise trip over.

Closes #355.

Changes

  • hookEventExists gains optional matcher and command assertions. Matchers compare as raw strings — the regex is not interpreted, since Claude Code evaluates it at runtime. When both are given they must be satisfied by the same hook group. A mismatch warns rather than fails: the registration exists, and a narrowed matcher may be deliberate. Absence still fails.
  • When settings merge drops a hook group because another source already registered the same command, the drop is now reported with both matchers named. Dedup itself is unchanged — it is what stops a hook firing twice — but it was previously invisible, most consequentially in the global scope where composition starts from the user's own settings file.
  • Precedence between a component-declared hook and a pack's settings file is now decided by the engine rather than by the order the components happen to appear in techpack.yaml.

Documented alongside the schema: these fields prove the declared matcher reached the settings file, not that it matches a tool Claude Code actually emits. Only a session transcript settles that.

Test plan

  • swift test passes locally
  • swiftformat --lint . and swiftlint pass without violations
  • With a pack that ships a hook via settingsFile: and declares matcher: on a hookEventExists check: run mcs sync, then mcs doctor → expect pass. Edit the matcher in .claude/settings.local.json by hand, re-run mcs doctor → expect a warning naming both the declared and the found matcher, and no failure.
  • With a pack declaring the same hook command as both a hook: component and an entry in its settings file, under differing matchers: run mcs sync → expect a warning that the settings-file group was not merged, and the component's matcher on disk.
Checklist for engine changes
  • Integration tests updated for new features (LifecycleIntegrationTests or DoctorRunnerIntegrationTests)
  • Docs updated if behavior changed (CLAUDE.md, docs/, techpack.yaml schema in ExternalPackManifest.swift)

- Add optional `matcher` and `command` fields so packs can verify hooks
  they ship through a settings file, which no derived check can see
- Report hook groups dropped by settings merge dedup instead of
  discarding them silently
- Compose derived hook entries before pack settings files, so precedence
  no longer depends on component order in techpack.yaml

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves doctor verification for hooks registered via pack-supplied settings files by enhancing hookEventExists to optionally assert the expected hook group matcher and a hook command substring, and by surfacing previously-silent hook group drops during settings composition.

Changes:

  • Extend hookEventExists doctor checks with optional matcher and command (substring) assertions, warning (not failing) on mismatches while still failing on missing registrations.
  • Make settings composition explicitly prefer derived (component-declared) hook registrations over settings-file copies, and warn when settings-merge dedup drops a group due to a command collision with a differing matcher.
  • Add shared matcher normalization/formatting helpers, plus unit/integration test coverage and schema documentation for the new behavior.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated no comments.

Show a summary per file
File Description
Tests/MCSTests/SettingsMergeTests.swift Adds tests covering hook-group dedup behavior and conflict reporting during settings merges.
Tests/MCSTests/PackHeuristicsTests.swift Adds tests for warnings when matcher is declared on doctor-check types that ignore it.
Tests/MCSTests/LifecycleIntegrationTests.swift Adds lifecycle coverage for settings-file hook matcher assertions and deterministic precedence vs component-derived hooks.
Tests/MCSTests/ExternalPackManifestTests.swift Adds validation tests for hookEventExists matcher/command fields.
Tests/MCSTests/ExternalDoctorCheckTests.swift Verifies factory wiring of matcher and command into the hookEventExists check.
Tests/MCSTests/CoreDoctorCheckSandboxTests.swift Adds sandbox tests for matcher/command assertion semantics and precedence of project vs global settings.
Sources/mcs/Sync/ConfiguratorSupport.swift Changes settings composition to two-pass merge and emits warnings for dropped conflicting hook groups.
Sources/mcs/ExternalPack/PackHeuristics.swift Adds heuristic warning when matcher is declared on doctor checks that don’t consult it.
Sources/mcs/ExternalPack/ExternalPackManifest.swift Validates non-empty matcher/command for hookEventExists and adds an explicit init with defaults to keep construction sites stable.
Sources/mcs/ExternalPack/ExternalDoctorCheck.swift Implements matcher/command assertions for hookEventExists and updates the factory to forward the new fields.
Sources/mcs/Doctor/CoreDoctorChecks.swift Switches matcher normalization/description to shared helper functions.
Sources/mcs/Core/Settings.swift Makes merge(with:) return dropped hook-group conflicts and introduces shared matcher normalization/description helpers.
docs/techpack-schema.md Documents hookEventExists matcher/command semantics and limitations.
Comments suppressed due to low confidence (1)

Sources/mcs/ExternalPack/ExternalDoctorCheck.swift:350

  • isOptional is not honored when no settings file exists: the check always returns .fail("no settings file found...") even if isOptional == true. This contradicts the documented semantics that optional checks downgrade absence to a skip, and it makes optional hook assertions fail on fresh installs where settings haven’t been created yet.
        guard probe.anyFileExisted else {
            return .fail("no settings file found (searched \(searchedFileNames))")
        }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@bguidolim
bguidolim merged commit 9bfeca6 into main Jul 30, 2026
5 checks passed
@bguidolim
bguidolim deleted the bruno/ISSUE-355-hook-matcher-assertion branch July 30, 2026 21:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

hookEventExists should be able to assert the matcher and command

2 participants