Skip to content

[BUG] Combined hook manifests (claude-codex-hooks.json) route to one target only; claude silently gets Copilot-schema fallback hooks #2020

Description

@garyj

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

  1. mkdir /tmp/repro && cd /tmp/repro
  2. apm install DietrichGebert/ponytail -t claude
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions