Hook command dedup ignores env expansion, dropping plugin hooks

Resolved 💬 7 comments Opened Jan 8, 2026 by alleneubank Closed Feb 22, 2026

Summary
Hooks are deduplicated by the raw command string (pre–env expansion). When multiple plugins use the same command string (e.g. ${CLAUDE_PLUGIN_ROOT}/hooks/stop-hook.ts), only one hook runs even though the expanded paths differ per plugin.

Steps to reproduce

  1. Create two plugins (A and B).
  2. In each plugin’s hooks/hooks.json, register a Stop hook with the same command string:

``json
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/stop-hook.ts"
}
]
}
]
}
}
``

  1. Enable both plugins.
  2. Trigger a Stop event.
  3. Inspect debug logs.

Expected
Both hooks execute (the expanded command paths are different per plugin).

Actual
Only one hook runs. Debug shows "Matched 1 unique hooks (2 before deduplication)".

Impact
Cross‑plugin hooks silently conflict, especially for Stop hooks (e.g., review gates, safety checks). This can disable critical behavior without warning.

Suggestions

  • Deduplicate after env expansion, or
  • Include plugin identity (plugin root/name) in the dedupe key.
  • If current behavior is intentional, document the requirement to make hook commands unique (e.g., distinct filenames or args).

View original on GitHub ↗

This issue has 7 comments on GitHub. Read the full discussion on GitHub ↗