Describe the bug
Filename-based hook routing recognizes only single-token stems. In
hook_file_routing.py,
_hook_file_allowed_targets matches <token>-hooks, *-<token>-hooks, or hooks-<token>, so a combined
manifest named claude-codex-hooks.json (a real authoring pattern: both .claude-plugin/plugin.json and
.codex-plugin/plugin.json in DietrichGebert/ponytail point their hooks at this one file) matches only via
endswith("-codex-hooks") and routes to codex alone -- never claude.
The claude target then falls back to the universal .apm/hooks/hooks.json. That file is generated during plugin
normalization from the first plugin.json found by find_plugin_json
(helpers.py), whose precedence puts
.github/plugin/plugin.json (Copilot) above .claude-plugin/plugin.json. Net effect: Copilot-schema hooks
(camelCase events, bash/powershell/timeoutSec, no matcher nesting) are written into .claude/settings.json,
where Claude Code silently ignores them, and the events that exist only in the Claude manifest (SubagentStart)
are dropped entirely.
A fix needs the stem matcher to recognize multi-token combined names: when every segment before -hooks is a
known target token, route to the union of those targets (claude-codex-hooks -> claude + codex).
To Reproduce
mkdir /tmp/repro && cd /tmp/repro
apm install DietrichGebert/ponytail -t claude
- Inspect
.claude/settings.json:
{
"hooks": {
"sessionStart": [
{ "type": "command",
"bash": "node \".claude/hooks/ponytail/hooks/ponytail-activate.js\"",
"powershell": "node \".claude/hooks/ponytail/hooks/ponytail-activate.js\"",
"timeoutSec": 5 } ],
"userPromptSubmitted": [ { "...same shape..." : "" } ]
}
}
Expected behavior
claude-codex-hooks.json is selected for both claude and codex. .claude/settings.json receives PascalCase
nested hooks (SessionStart / SubagentStart / UserPromptSubmit with matcher/hooks entries), matching what
the codex target already receives from the same file.
Environment
- OS: Linux
- Python Version: 3.13
- APM Version: 0.23.1 (also reproduced on main @ 13e96d1)
Logs
Hook events for target 'claude' may not be recognized: sessionStart, userPromptSubmitted. Target expects
PascalCase (e.g. PreToolUse). Rename events to match the PascalCase convention, then reinstall.
WARNING apm_cli.integration.hook_integrator target claude: hook event casing mismatch (no mapping): sessionStart, userPromptSubmitted
Additional context
Same routing subsystem as #1892 / #1902 (which introduced filename routing and its deprecation path) and the
mirror image of #1977 / #1979 on the copilot side. The recommended object-form workaround (targets: [claude, codex] on the dependency) does not avoid this: it bypasses filename routing entirely and deploys all of the
package's hook files to claude, so the correct PascalCase hooks arrive together with the Copilot-schema entries.
The target-agnostic find_plugin_json precedence that produces the Copilot-shaped universal fallback is arguably
a separate design issue; happy to file it separately. PR with fix + regression tests incoming.
Describe the bug
Filename-based hook routing recognizes only single-token stems. In
hook_file_routing.py,_hook_file_allowed_targetsmatches<token>-hooks,*-<token>-hooks, orhooks-<token>, so a combinedmanifest named
claude-codex-hooks.json(a real authoring pattern: both.claude-plugin/plugin.jsonand.codex-plugin/plugin.jsonin DietrichGebert/ponytail point theirhooksat this one file) matches only viaendswith("-codex-hooks")and routes to codex alone -- never claude.The claude target then falls back to the universal
.apm/hooks/hooks.json. That file is generated during pluginnormalization from the first
plugin.jsonfound byfind_plugin_json(
helpers.py), whose precedence puts.github/plugin/plugin.json(Copilot) above.claude-plugin/plugin.json. Net effect: Copilot-schema hooks(camelCase events,
bash/powershell/timeoutSec, no matcher nesting) are written into.claude/settings.json,where Claude Code silently ignores them, and the events that exist only in the Claude manifest (
SubagentStart)are dropped entirely.
A fix needs the stem matcher to recognize multi-token combined names: when every segment before
-hooksis aknown target token, route to the union of those targets (
claude-codex-hooks-> claude + codex).To Reproduce
mkdir /tmp/repro && cd /tmp/reproapm install DietrichGebert/ponytail -t claude.claude/settings.json:{ "hooks": { "sessionStart": [ { "type": "command", "bash": "node \".claude/hooks/ponytail/hooks/ponytail-activate.js\"", "powershell": "node \".claude/hooks/ponytail/hooks/ponytail-activate.js\"", "timeoutSec": 5 } ], "userPromptSubmitted": [ { "...same shape..." : "" } ] } }Expected behavior
claude-codex-hooks.jsonis selected for both claude and codex..claude/settings.jsonreceives PascalCasenested hooks (
SessionStart/SubagentStart/UserPromptSubmitwithmatcher/hooksentries), matching whatthe codex target already receives from the same file.
Environment
Logs
Additional context
Same routing subsystem as #1892 / #1902 (which introduced filename routing and its deprecation path) and the
mirror image of #1977 / #1979 on the copilot side. The recommended object-form workaround (
targets: [claude, codex]on the dependency) does not avoid this: it bypasses filename routing entirely and deploys all of thepackage's hook files to claude, so the correct PascalCase hooks arrive together with the Copilot-schema entries.
The target-agnostic
find_plugin_jsonprecedence that produces the Copilot-shaped universal fallback is arguablya separate design issue; happy to file it separately. PR with fix + regression tests incoming.