Skip to content

fix: correct Claude settings.json hook emission (#1007)#1054

Merged
danielmeppiel merged 4 commits intomainfrom
fix/1007-claude-settings-hook-emission
Apr 30, 2026
Merged

fix: correct Claude settings.json hook emission (#1007)#1054
danielmeppiel merged 4 commits intomainfrom
fix/1007-claude-settings-hook-emission

Conversation

@sergio-sisternes-epam
Copy link
Copy Markdown
Collaborator

Description

Fixes four bugs in .claude/settings.json hook emission when installing multi-target hook packages (e.g. microsoft/azure-skills):

  1. Cursor variable leak -- ${CURSOR_PLUGIN_ROOT} emitted into Claude's config (Claude does not expand it)
  2. Bare ${PLUGIN_ROOT} -- unrecognised variable passes through unchanged
  3. Mixed-case event keys -- both PostToolUse and postToolUse appear; Claude only reads PascalCase
  4. Duplicate entries -- each hook appears twice from a single install

Fixes #1007

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Maintenance / refactor

Approach

Four surgical fixes in hook_integrator.py:

Fix What How
Target routing Cursor hooks leak into Claude config _filter_hook_files_for_target() routes *-cursor-hooks.json only to Cursor, *-claude-hooks.json only to Claude, etc. Generic hooks.json deploys everywhere.
Variable expansion ${PLUGIN_ROOT} and ${CURSOR_PLUGIN_ROOT} pass through Regex now matches all three variable conventions via alternation group
Event normalisation Both postToolUse and PostToolUse keys in Claude config Added Claude entry in _HOOK_EVENT_MAP mapping camelCase to PascalCase
Deduplication Same hook appears twice from multiple source files Content-based dedup scoped to same-package entries preserves cross-package ownership

Additionally, alias-aware clearing ensures reinstalls correctly remove entries written under previously leaked event name variants.

Testing

  • Tested locally
  • All existing tests pass (118 hook tests, 6701 full unit suite)
  • Added tests for new functionality -- 18 new test cases in TestIssue1007Fixes class covering:
    • Target-aware routing (6 tests)
    • Variable pattern expansion (4 tests)
    • Event name normalisation (3 tests)
    • Content-based deduplication (4 tests)
    • Full pipeline regression (1 test)

Fix four bugs in .claude/settings.json hook emission when installing
multi-target hook packages:

1. Target-aware hook file routing -- files named *-cursor-hooks.json,
   *-claude-hooks.json etc. are now routed only to their intended target
   instead of leaking into every target's config.

2. Expanded variable pattern recognition -- bare ${PLUGIN_ROOT} and
   ${CURSOR_PLUGIN_ROOT} are now correctly rewritten alongside
   ${CLAUDE_PLUGIN_ROOT} during target-specific command rewriting.

3. Event name normalisation for Claude -- camelCase event names
   (postToolUse, preToolUse) from Copilot/Cursor hook files are
   normalised to PascalCase (PostToolUse, PreToolUse) for Claude.

4. Content-based deduplication -- duplicate hook entries from multiple
   source files targeting the same event are collapsed (scoped to
   same-package entries to preserve cross-package ownership).

Includes alias-aware clearing so reinstalls correctly remove entries
written under previously leaked event name variants.

Closes #1007

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@sergio-sisternes-epam
Copy link
Copy Markdown
Collaborator Author

APM Review Panel Verdict: REJECT

Architecture is sound and well-tested, but the missing apm-usage skill resource update blocks merge per Rule 4.

Required before merge (1 item)

  • [devx-ux-expert] apm-usage skill resource not updated for hook file routing convention at packages/apm-guide/.apm/skills/apm-usage/package-authoring.md
    • Why: The PR adds a new naming convention that package authors must follow (target-specific hook filenames like *-cursor-hooks.json). Rule 4 mandates that primitive file format changes also update packages/apm-guide/.apm/skills/apm-usage/package-authoring.md. An agent relying on the apm-usage skill to author a package will not discover this convention, leading to the exact cross-target leak bugs this PR fixes.
    • Suggested fix: Add a short section (3-5 lines + the table) under the existing primitives documentation in package-authoring.md explaining the target-specific hook filename convention and which suffixes route to which targets.

Nits (16 items, skip if you want)

  • [python-architect] reverse_map rebuilt on every hook-file iteration; hoist above the loop at hook_integrator.py:649
  • [python-architect] Mixed union syntax: str | None vs Optional[str] in the same file at hook_integrator.py:210
  • [python-architect] Dedup seen tracker is O(n^2) list scan; fine at current scale but a set would be clearer intent at hook_integrator.py:701
  • [python-architect] Test class TestIssue1007Fixes uses tmp_path while legacy classes use manual tempfile.mkdtemp
  • [cli-logging-expert] Silent file routing has no verbose diagnostic at hook_integrator.py:216
  • [cli-logging-expert] Silent deduplication loses diagnostic signal at hook_integrator.py:714
  • [cli-logging-expert] Adjacent pre-existing em-dash not cleaned up at hook_integrator.py:614
  • [cli-logging-expert] Test file adds new non-ASCII characters (en dashes, arrows, box-drawing) in docstrings at test_hook_integrator.py:2614
  • [devx-ux-expert] Doc table last row wording is ambiguous for package authors at plugins.md:224
  • [devx-ux-expert] No debug/warning log when hook files are filtered out at hook_integrator.py:215
  • [devx-ux-expert] Dedup comparison uses O(n^2) linear scan at hook_integrator.py:701
  • [devx-ux-expert] CHANGELOG entry should be checked for ASCII compliance at CHANGELOG.md:40
  • [supply-chain-security-expert] Dedup exclusion filter hard-codes _apm_source; prefix-based exclusion would be more resilient at hook_integrator.py:708
  • [oss-growth-hacker] CHANGELOG entry buries four fixes in one dense sentence -- suggest bulleted sub-list at CHANGELOG.md:40
  • [oss-growth-hacker] New convention lacks a runnable directory tree example in docs at plugins.md:212
  • [oss-growth-hacker] New convention not reflected in apm-usage/package-authoring.md skill resource

CEO arbitration

This is a well-scoped, well-tested bug fix that closes a real user-reported issue (#1007). The four fixes are surgical, the 18 new tests provide strong regression coverage, and the docs/guides/plugins.md update documents the new convention. The panelists converged on the quality of the implementation -- no REQUIRED findings on the code itself. The sole REQUIRED finding comes from DevX: the apm-usage skill resource (package-authoring.md) was not updated with the new target-specific hook filename convention. I uphold this as REQUIRED. Rule 4 is an explicit written instruction in this repository, the convention is genuinely new (not a fix to existing behaviour), and the apm-usage skill is the primary surface AI agents consult when authoring packages. Without the update, agents will never discover that naming a file *-claude-hooks.json routes it exclusively to Claude. The fix is low-effort -- a short section in package-authoring.md mirroring the table already added to plugins.md. Ship this once that gap is closed.

On nits: the non-ASCII characters in the test file (box-drawing, en dashes, arrows in docstrings and section banners) violate the project encoding rule which scopes to all source files, not just CLI output. These should be cleaned up in this PR since the test file is new code being added. The O(n^2) dedup scan noted by three panelists is fine at current hook-count scale. The CHANGELOG single-line entry follows Keep a Changelog convention (one line per PR) but could benefit from the Growth Hacker's bulleted sub-list suggestion given that four distinct fixes are packed into one sentence -- that is an author-discretion nit, not a blocker.

Dissent resolved: DevX classified the missing apm-usage/package-authoring.md update as REQUIRED; OSS Growth Hacker echoed the same gap but classified it as a nit. I side with DevX. Rule 4 is not discretionary guidance -- it is a mandatory instruction checked by the project's own conventions. The convention is new, not pre-existing, so the omission is attributable to this PR.

Growth/positioning note: The Growth Hacker's observation about a runnable directory-tree example in the docs is worth amplifying. Package authors seeing a concrete tree (e.g. my-package/hooks/copilot-hooks.json, cursor-hooks.json, hooks.json) will grok the convention faster than a table alone. Consider adding one to either plugins.md or package-authoring.md.


Per-persona findings (full)

Python Architect

classDiagram
    direction LR
    class BaseIntegrator {
        check_collision()
        cleanup_empty_parents()
        should_integrate()
    }
    class HookIntegrator {
        +HOOK_COMMAND_KEYS Tuple
        +find_hook_files(package_path) List~Path~
        +integrate_package_hooks()
        +integrate_hooks_for_target()
        -_integrate_merged_hooks()
        -_rewrite_hooks_data()
        -_rewrite_command_for_target()
        -_parse_hook_json()
        +sync_integration()
    }
    class _MergeHookConfig {
        +config_filename str
        +target_key str
        +require_dir bool
    }
    class _filter_hook_files_for_target {
        +__call__(hook_files, target_key) List~Path~
    }
    class _HOOK_EVENT_MAP {
        +claude dict
        +gemini dict
    }
    class _HOOK_FILE_TARGET_SUFFIXES {
        +copilot-hooks set
        +cursor-hooks set
        +claude-hooks set
        +codex-hooks set
        +gemini-hooks set
    }
    BaseIntegrator <|-- HookIntegrator
    HookIntegrator *-- _MergeHookConfig : uses via _MERGE_HOOK_TARGETS
    HookIntegrator ..> _filter_hook_files_for_target : calls at 2 sites
    HookIntegrator ..> _HOOK_EVENT_MAP : reads during merge
    _filter_hook_files_for_target ..> _HOOK_FILE_TARGET_SUFFIXES : reads
Loading
  • Nit: reverse_map rebuilt on every hook-file iteration; hoist above the loop (line 649)
  • Nit: Mixed union syntax str | None vs Optional[str] in the same file (line 210)
  • Nit: Dedup seen tracker is O(n^2) list scan; fine at current scale (line 701)
  • Nit: Test class uses tmp_path while legacy classes use manual tempfile.mkdtemp

CLI Logging Expert

  • Nit: Silent file routing has no verbose diagnostic (line 216)
  • Nit: Silent deduplication loses diagnostic signal (line 714)
  • Nit: Adjacent pre-existing em-dash not cleaned up (line 614)
  • Nit: Test file adds new non-ASCII characters in docstrings (line 2614)

DevX UX Expert

  • REQUIRED: apm-usage skill resource not updated for hook file routing convention (package-authoring.md)
  • Nit: Doc table last row wording is ambiguous for package authors (plugins.md:224)
  • Nit: No debug/warning log when hook files are filtered out (line 215)
  • Nit: Dedup comparison uses O(n^2) linear scan (line 701)
  • Nit: CHANGELOG entry should be checked for ASCII compliance (line 40)

Supply Chain Security Expert

  • Nit: Dedup exclusion filter hard-codes _apm_source; prefix-based exclusion would be more resilient (line 708)

Auth Expert

Inactive -- PR touches only hook_integrator.py, test_hook_integrator.py, CHANGELOG.md, and plugins.md -- no auth, token, credential, or host classification files changed.

OSS Growth Hacker

  • Nit: CHANGELOG entry buries four fixes in one dense sentence -- suggest bulleted sub-list
  • Nit: New convention lacks a runnable directory tree example in docs
  • Nit: New convention not reflected in apm-usage/package-authoring.md skill resource

Verdict computed deterministically: 1 required finding across 5 active panelists. APPROVE iff N == 0. Push a new commit to clear this verdict label automatically.

…ASCII encoding

- Add target-specific hook file naming section to
  packages/apm-guide/.apm/skills/apm-usage/package-authoring.md
  (addresses REQUIRED panel finding from DevX UX Expert re Rule 4)
- Replace non-ASCII characters (box-drawing, en dashes, em dashes,
  arrows) with ASCII equivalents in test docstrings and comments
- Fix pre-existing em-dash in hook_integrator.py comment (line 614)
- Improve docs table clarity and add directory tree example

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@sergio-sisternes-epam
Copy link
Copy Markdown
Collaborator Author

Panel findings addressed (714531a)

All findings from the review panel have been addressed:

REQUIRED (resolved):

  • Added target-specific hook file naming section to packages/apm-guide/.apm/skills/apm-usage/package-authoring.md (DevX UX Expert / Rule 4)

Nits adopted:

  • Replaced all non-ASCII characters in test docstrings/comments with ASCII equivalents (CLI Logging Expert)
  • Fixed pre-existing em-dash on line 614 of hook_integrator.py (CLI Logging Expert)
  • Improved docs table clarity -- catch-all row now reads "Any other name" (DevX UX Expert)
  • Added directory tree example to both plugins.md and package-authoring.md (OSS Growth Hacker)

Nits deferred (follow-up):

  • reverse_map loop hoisting (Python Architect) -- correct but minor, future PR
  • O(n^2) dedup scan (Python Architect, DevX) -- fine at current scale
  • Debug logging for filtered/deduped hooks (CLI Logging, DevX) -- good idea, separate PR
  • Prefix-based _apm_ exclusion in dedup (Supply Chain) -- separate hardening PR
  • CHANGELOG bullet list format (OSS Growth) -- single-line matches Keep a Changelog convention
  • Union syntax consistency (Python Architect) -- file-wide cleanup, separate PR

@sergio-sisternes-epam sergio-sisternes-epam added the panel-review Trigger the apm-review-panel gh-aw workflow label Apr 29, 2026
@github-actions
Copy link
Copy Markdown

APM Review Panel Verdict: APPROVE

Re-review triggered by commit 714531ae which addressed all findings from the prior panel (REJECT, 2026-04-29T22:00).

All REQUIRED findings are resolved. No new REQUIRED findings identified.

Prior REQUIRED finding — resolved ✅

  • [devx-ux-expert] packages/apm-guide/.apm/skills/apm-usage/package-authoring.md now includes the Hook files section with the routing table and a directory-tree example. The new convention is discoverable by agents consulting the apm-usage skill.

Nits addressed since prior review ✅

Nit Source Status
Non-ASCII chars in new test docstrings CLI Logging Expert Resolved — TestIssue1007Fixes class is ASCII-clean
Pre-existing em-dash on hook_integrator.py:614 CLI Logging Expert Resolved — --
Doc table catch-all row wording DevX UX Expert Resolved — "Any other name"
Directory tree example in docs OSS Growth Hacker Resolved — added to both plugins.md and package-authoring.md

Deferred nits (follow-up PRs, not blocking)

Nit Owner
reverse_map rebuilt on every hook-file iteration; hoist above loop Python Architect
O(n2) dedup scan (seen_content list) Python Architect / DevX
Debug/warning log when files are filtered or entries deduped CLI Logging / DevX
Prefix-based _apm_ exclusion in dedup filter Supply Chain Security
CHANGELOG bulleted sub-list for four fixes OSS Growth Hacker
str | None vs Optional[str] union syntax consistency Python Architect

CEO arbitration

The prior REJECT was on a single, low-effort gap: the apm-usage skill resource lacked the new target-specific hook filename convention. That gap is closed. The four bug-fixes remain well-scoped, well-tested (18 new regression tests covering all fix dimensions), and the documentation is now coherent across plugins.md and package-authoring.md. The deferred nits are correctly characterised: none is a correctness issue at current hook-count scale, and each maps naturally to a follow-up hardening PR.

The PR is a draft and has a dirty merge state — those are operational matters for the author to resolve before merge, not panel concerns.

Panel approves. Clear merge conflicts and mark ready for review to proceed.


Per-persona findings (full)

Python Architect

No new REQUIRED findings. Architecture is correct: _filter_hook_files_for_target is a pure, testable function; _HOOK_FILE_TARGET_SUFFIXES is a clean data table; the two call sites in integrate_package_hooks (hardcoded "copilot") and _integrate_merged_hooks (uses config.target_key) are both correct. Deferred nits: reverse_map hoisting, O(n2) dedup, union syntax.

CLI Logging Expert

No new REQUIRED findings. Em-dash cleaned. New test class is ASCII-clean. Pre-existing non-ASCII section banners (box-drawing chars in lines 34–2495) are out-of-scope for this PR. Deferred: debug logging for filtered/deduped hooks.

DevX UX Expert

No new REQUIRED findings. Rule 4 satisfied: package-authoring.md documents the naming convention with table and directory tree. Catch-all row reads "Any other name" — unambiguous. Deferred: O(n2) dedup.

Supply Chain Security Expert

No new REQUIRED findings. _apm_source field key is an internal APM annotation; hard-coding it is acceptable. Deferred: prefix-based exclusion for resilience.

Auth Expert

Inactive — no auth, token, credential, or host classification files changed.

OSS Growth Hacker

No new REQUIRED findings. Directory tree examples present in both docs. CHANGELOG single-line follows Keep a Changelog convention (author discretion to expand). Deferred: bulleted sub-list.

Verdict computed deterministically: 0 required findings across 5 active panelists. APPROVE iff N == 0. ✅

Generated by PR Review Panel for issue #1054 · ● 634.5K ·

@github-actions github-actions Bot added panel-approved Apm-review-panel verdict: APPROVE. Removed automatically on next push. and removed panel-review Trigger the apm-review-panel gh-aw workflow labels Apr 29, 2026
@danielmeppiel danielmeppiel marked this pull request as ready for review April 30, 2026 05:08
@danielmeppiel danielmeppiel self-requested a review as a code owner April 30, 2026 05:08
Copilot AI review requested due to automatic review settings April 30, 2026 05:08
@github-actions github-actions Bot removed the panel-approved Apm-review-panel verdict: APPROVE. Removed automatically on next push. label Apr 30, 2026
@danielmeppiel danielmeppiel enabled auto-merge April 30, 2026 05:10
@danielmeppiel danielmeppiel disabled auto-merge April 30, 2026 05:12
@danielmeppiel danielmeppiel merged commit 95cc746 into main Apr 30, 2026
8 checks passed
@danielmeppiel danielmeppiel deleted the fix/1007-claude-settings-hook-emission branch April 30, 2026 05:12
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 fixes .claude/settings.json hook emission when installing multi-target hook packages by ensuring only target-appropriate hook files are merged, normalizing event names for Claude, expanding additional plugin-root variable forms, and preventing duplicate hook entries.

Changes:

  • Add target-aware routing for hook JSON files via filename stem suffixes (e.g., *-cursor-hooks.json only to Cursor; generic hooks.json to all).
  • Expand plugin-root variable rewriting to handle ${PLUGIN_ROOT} and ${CURSOR_PLUGIN_ROOT} in addition to ${CLAUDE_PLUGIN_ROOT}, and normalize Claude event keys (camelCase -> PascalCase).
  • Add regression tests for issue #1007 and document target-specific hook filename conventions.
Show a summary per file
File Description
src/apm_cli/integration/hook_integrator.py Adds hook-file routing by target, Claude event mapping, expanded plugin-root variable matching, alias-aware clearing, and content-based dedup logic.
tests/unit/integration/test_hook_integrator.py Adds a new regression test class covering routing, variable rewriting, event normalization, deduplication, and reinstall behavior.
packages/apm-guide/.apm/skills/apm-usage/package-authoring.md Documents target-specific hook filenames and notes event/variable normalization behavior.
docs/src/content/docs/guides/plugins.md Documents target-specific hook filenames in the plugin authoring guide.

Copilot's findings

  • Files reviewed: 4/4 changed files
  • Comments generated: 1

Comment on lines +701 to +716
seen_content: list[dict] = []
deduped: list = []
for entry in json_config["hooks"][event_name]:
if not isinstance(entry, dict):
deduped.append(entry)
continue
# Build comparison key (all fields except _apm_source)
cmp = {k: v for k, v in sorted(entry.items()) if k != "_apm_source"}
source = entry.get("_apm_source")
is_dup = False
for seen in seen_content:
if seen.get("_source") == source and seen.get("_cmp") == cmp:
is_dup = True
break
if not is_dup:
seen_content.append({"_source": source, "_cmp": cmp})
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

The new content-based deduplication runs over the entire event list and dedups entries for any _apm_source value (including other packages) and also user-authored entries that have no _apm_source (treated as None). This can unintentionally remove duplicates that are not owned by the package currently being installed. Dedup should be scoped to the current package only (e.g., only consider/remove duplicates among entries where _apm_source == package_name, leaving other entries untouched).

Suggested change
seen_content: list[dict] = []
deduped: list = []
for entry in json_config["hooks"][event_name]:
if not isinstance(entry, dict):
deduped.append(entry)
continue
# Build comparison key (all fields except _apm_source)
cmp = {k: v for k, v in sorted(entry.items()) if k != "_apm_source"}
source = entry.get("_apm_source")
is_dup = False
for seen in seen_content:
if seen.get("_source") == source and seen.get("_cmp") == cmp:
is_dup = True
break
if not is_dup:
seen_content.append({"_source": source, "_cmp": cmp})
#
# Scope deduplication to entries owned by the package being
# installed. Leave entries from other packages and
# user-authored entries without _apm_source untouched.
seen_content: list[dict] = []
deduped: list = []
for entry in json_config["hooks"][event_name]:
if not isinstance(entry, dict):
deduped.append(entry)
continue
if entry.get("_apm_source") != package_name:
deduped.append(entry)
continue
# Build comparison key for package-owned entries
# only (all fields except _apm_source).
cmp = {
k: v for k, v in sorted(entry.items()) if k != "_apm_source"
}
is_dup = False
for seen in seen_content:
if seen == cmp:
is_dup = True
break
if not is_dup:
seen_content.append(cmp)

Copilot uses AI. Check for mistakes.
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.

[BUG] .claude/settings.json emits Cursor-formatted hooks, bare ${PLUGIN_ROOT}, and duplicate entries after fresh install of microsoft/azure-skills

3 participants