Skip to content

[BUG] Hooks for target cursor are written in Claude format; Cursor rejects the whole .cursor/hooks.json #3129

Open

Description

Describe the bug

For the cursor target, apm install writes Claude-shaped hook entries into .cursor/hooks.json unchanged: PascalCase event keys (PreToolUse, UserPromptSubmit, PreCompact) and nested { "matcher", "hooks": [{ "type", "command", "timeout" }] } groups. Only script paths are rewritten and "version": 1 is added. Events are not renamed and entries are not reshaped for Cursor.

In src/apm_cli/integration/hook_integrator.py (v0.32.0 and current main):

  • _HOOK_EVENT_MAP has no cursor entry.
  • The merge loop has native renderers for claude, codex, gemini and antigravity, but none for cursor.
  • _HOOK_EVENT_EXPECTED_CASING["cursor"] is "PascalCase", but Cursor's event names are camelCase.

The docs promise otherwise (producer/author-primitives/hooks-and-commands):

APM accepts the Claude (PreToolUse, PostToolUse) and Copilot (preToolUse, postToolUse) shapes; events are renamed per target during merge.

Merged hook files contain only each target's native upstream fields.

Cursor validates project hooks.json strictly. A single unknown event key makes the whole file invalid, and none of its hooks are loaded. This includes hand-written entries in the same file, because APM merges into it. #2321 described Claude-format keys in .cursor/hooks.json as dead but non-fatal config. With Cursor 3.13.10 they disable every project hook.

The casing warning also points the wrong way. A correct Cursor event in a hook file (for example preToolUse) gets this warning:

Hook events for target 'cursor' may not be recognized: preToolUse. Target
expects PascalCase (e.g. PreToolUse). Rename events to match the PascalCase
convention, then reinstall.

Following that advice produces a file that Cursor rejects.

To Reproduce

C:\work\demo-pkg\apm.yml

name: demo-pkg
version: 0.0.1
targets: [claude, codex, cursor]

C:\work\demo-pkg\.apm\hooks\gate.json

{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Edit|Write|mcp__ide__.*", "hooks": [ { "type": "command", "command": "node \"${PLUGIN_ROOT}/.apm/hooks/scripts/gate.mjs\"", "timeout": 10 } ] },
      { "matcher": "Bash", "hooks": [ { "type": "command", "command": "node \"${PLUGIN_ROOT}/.apm/hooks/scripts/gate.mjs\"", "timeout": 10 } ] }
    ],
    "UserPromptSubmit": [ { "hooks": [ { "type": "command", "command": "node \"${PLUGIN_ROOT}/.apm/hooks/scripts/gate.mjs\"" } ] } ],
    "PreCompact": [ { "hooks": [ { "type": "command", "command": "node \"${PLUGIN_ROOT}/.apm/hooks/scripts/gate.mjs\"" } ] } ]
  }
}

C:\work\demo-pkg\.apm\hooks\scripts\gate.mjs

process.exit(0);

C:\work\consumer\apm.yml

name: consumer
version: 0.0.1
targets: [claude, codex, cursor]
dependencies:
  apm:
    - path: ../demo-pkg
cd C:\work\consumer
git init
apm install --target claude,codex,cursor

The install succeeds (exit code 0). The command prints no warning for the cursor target.

Actual .cursor/hooks.json

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write|mcp__ide__.*",
        "hooks": [
          { "type": "command", "command": "node \".cursor/hooks/demo-pkg/.apm/hooks/scripts/gate.mjs\"", "timeout": 10 }
        ]
      },
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "node \".cursor/hooks/demo-pkg/.apm/hooks/scripts/gate.mjs\"", "timeout": 10 }
        ]
      }
    ],
    "UserPromptSubmit": [
      { "hooks": [ { "type": "command", "command": "node \".cursor/hooks/demo-pkg/.apm/hooks/scripts/gate.mjs\"" } ] }
    ],
    "PreCompact": [
      { "hooks": [ { "type": "command", "command": "node \".cursor/hooks/demo-pkg/.apm/hooks/scripts/gate.mjs\"" } ] }
    ]
  },
  "version": 1
}

I ran Cursor's own hooks.json validator (the function parseAndValidateHooksConfig calls, taken from out/vs/workbench/workbench.desktop.main.js of Cursor 3.13.10) on this file:

isValid: false
- Unknown hook type: PreToolUse. Valid types are: beforeShellExecution, beforeMCPExecution, afterShellExecution, afterMCPExecution, beforeReadFile, afterFileEdit, beforeTabFileRead, afterTabFileEdit, stop, beforeSubmitPrompt, afterAgentResponse, afterAgentThought, sessionStart, sessionEnd, preCompact, subagentStart, subagentStop, preToolUse, postToolUse, postToolUseFailure, workspaceOpen
- Unknown hook type: UserPromptSubmit. Valid types are: ...
- Unknown hook type: PreCompact. Valid types are: ...

When validation fails, parseAndValidateHooksConfig returns an error and the project config is not loaded at all.

Expected behavior

APM should write Cursor's native schema. For the repro above:

{
  "version": 1,
  "hooks": {
    "preToolUse": [
      { "type": "command", "command": "node \".cursor/hooks/demo-pkg/.apm/hooks/scripts/gate.mjs\"", "matcher": "Write|MCP:.*", "timeout": 10 },
      { "type": "command", "command": "node \".cursor/hooks/demo-pkg/.apm/hooks/scripts/gate.mjs\"", "matcher": "Shell", "timeout": 10 }
    ],
    "beforeSubmitPrompt": [
      { "type": "command", "command": "node \".cursor/hooks/demo-pkg/.apm/hooks/scripts/gate.mjs\"" }
    ],
    "preCompact": [
      { "type": "command", "command": "node \".cursor/hooks/demo-pkg/.apm/hooks/scripts/gate.mjs\"" }
    ]
  }
}

The same validator returns isValid: true for this file. The repo-relative command paths APM already writes keep working: for project hooks Cursor sets the working directory to the project root (_getHookCwd resolves <hooks.json>/../..).

Suggested mapping. This is the conversion Cursor itself applies when it imports Claude hooks from .claude/settings.json (packages/hooks/src/claude-code-types.ts in the same bundle):

Claude event Cursor event
PreToolUse preToolUse
PostToolUse postToolUse
UserPromptSubmit beforeSubmitPrompt
Stop stop
SubagentStop subagentStop
SessionStart sessionStart
SessionEnd sessionEnd
PreCompact preCompact
Notification, PermissionRequest not supported (skip with a warning)
Claude matcher tool (only on PreToolUse / PostToolUse) Cursor matcher
Bash Shell
Edit, Write Write
Read, Grep, Task, WebFetch, WebSearch unchanged
Glob not supported (dropped with a warning)
mcp__<server>__<tool> MCP:<tool> (the server part is dropped)
* or empty omit matcher
  • Entries: each nested handler becomes one flat entry: { "type": "command", "command", "matcher"?, "timeout"? }. timeout is in seconds in both schemas. Cursor's importer also maps type: "prompt" handlers and drops the other handler types.
  • Matchers on other events: Cursor's importer drops them.
  • Copilot-shaped sources: preToolUse, postToolUse and sessionStart already match Cursor's names. By the same logic, userPromptSubmitted would map to beforeSubmitPrompt and agentStop to stop.

Also needed: events for one target only

Translation alone does not let a single package drop its hand-maintained Cursor file. Some Cursor events have no Claude counterpart (beforeReadFile, beforeShellExecution, beforeMCPExecution, afterFileEdit, ...). They cannot be written in Claude shape and translated, so the package has to ship them for Cursor only, next to the translated common hooks. Today there are three ways to try this:

  • Universal file: the event goes to every target. I added .apm/hooks/read-gate.json to the repro:

    { "version": 1, "hooks": { "beforeReadFile": [ { "command": "node \"${PLUGIN_ROOT}/.apm/hooks/scripts/gate.mjs\"", "timeout": 10 } ] } }

    beforeReadFile then lands in .claude/settings.json (as { "matcher": "*", "hooks": [{ "command", "timeout" }] }) and .codex/hooks.json as well as .cursor/hooks.json. All three targets warn may not be recognized: beforeReadFile ... Rename events to match the PascalCase convention, but Claude and Codex have no event to rename it to.

  • Filename routing (*-cursor-hooks.json): the file reaches only Cursor, and the result passes Cursor's validator. But universal files are then skipped for Cursor, so the Cursor file has to repeat the common hooks by hand. That is the manual file we want to drop. Filename routing is also deprecated ([FEATURE] Deprecate and remove filename-based hook routing in favor of per-package targets: #2031).

  • Second package with targets: [cursor]: this works. Cursor gets the hooks of both packages, and Claude and Codex get only the common ones. The costs: one logical package becomes two, consumers have to declare both, and the second package needs its own copy of the hook scripts.

Request: let a package mark a hook file as target-specific, so that it is installed in addition to the universal files for that target. Done when the repro package plus read-gate.json marked for Cursor installs to:

  • a .cursor/hooks.json that passes Cursor's validator and contains both the translated gate.json entries and beforeReadFile;
  • a .claude/settings.json and .codex/hooks.json that contain only the gate.json entries.

The per-file targets rules proposed in #2261 (currently deferred) would satisfy this, because each file gets its own target list instead of replacing the universal files.

Environment (please complete the following information):

  • OS: Windows 11 Enterprise (10.0.26200)
  • Python Version: N/A (standalone Windows binary from the release zip)
  • APM Version: 0.32.0. 0.31.0 gives byte-identical output.
  • Cursor Version: 3.13.10 (commit 4f02290ccd9304f0e6bf8ee85f6e9106f02ac1f0)

Logs

Excerpt from apm install --target claude,codex,cursor --verbose:

Package target restriction: [claude, codex, cursor]; effective targets:
[claude, codex, cursor]
  |-- 3 hook(s) integrated:
  |     -> .claude/settings.json
  |     -> .codex/hooks.json
  |     -> .cursor/hooks.json
  ...
  |   PreToolUse: runs .cursor/hooks/demo-pkg/.apm/hooks/scripts/gate.mjs
(gate.json)
  |   PreToolUse: runs .cursor/hooks/demo-pkg/.apm/hooks/scripts/gate.mjs
(gate.json)
  |   UserPromptSubmit: runs .cursor/hooks/demo-pkg/.apm/hooks/scripts/gate.mjs
(gate.json)
  |   PreCompact: runs .cursor/hooks/demo-pkg/.apm/hooks/scripts/gate.mjs
(gate.json)
...
[*] Installed 1 APM dependency in 0.4s.

Additional context

Activity

  1. danielmeppiel commented on Oct 2, 2026

    @danielmeppiel
    Collaborator

    Decision: approve
    Area: project
    Scope: Repair Cursor hook installation so supported cases produce valid Cursor-native configuration using verified event, handler and matcher contracts. Preserve user-owned hooks, APM ownership, executable consent and restrictions; account for Cursor's native Claude-hook import when selecting the smallest compatible repair.
    Done when: Supported installed hooks produce output accepted by Cursor's native contract; install, reinstall and removal preserve unrelated user hooks; unsupported or unrepresentable behavior receives an explicit diagnostic without silent weakening or misleading success. Regression coverage checks native output and coexistence with Claude-hook import so corrected output does not newly double-activate hooks. Guidance states supported mappings and limitations.
    Out of scope: Invented or blanket case-converted event mappings, broadened matchers, claiming identical semantics across harnesses, silently skipping required behavior, a new target-specific hook-routing framework, the universal #2111 translation program, silently redirecting target scope, changing user import settings, or bypassing execution consent.
    Review contact: Daniel Meppiel (@danielmeppiel)

    The official Cursor hook reference documents the native shape, and its third-party hook reference documents supported Claude import mappings and gaps. Those are the compatibility authorities, not inferred equivalence between similarly named events.

    The reported whole-file rejection raises the priority of this existing-target repair. The pre-analysis verified the rendering gap and documentation, not a new Cursor runtime reproduction. Native Claude-hook import is relevant to preventing duplicate activation; this approval does not prescribe a competing conversion system when an existing supported route can meet the same bounded outcome.

    Recorded by Copilot on behalf of Daniel Meppiel (@danielmeppiel) after his explicit "ok to all yes" confirmation of the four priority batches on 2026-10-02. This records the human-approved scope, not an autonomous recommendation. It does not assign implementation, promise a release, approve a merge, or replace required review.

  2. danielmeppiel commented on Oct 2, 2026

    @danielmeppiel
    Collaborator

    Decision: approve
    Area: project
    Scope: Repair Cursor hook installation so supported cases produce valid Cursor-native configuration using verified event, handler and matcher contracts. Preserve user-owned hooks, APM ownership, executable consent and restrictions; account for Cursor's native Claude-hook import when selecting the smallest compatible repair.
    Done when: Supported installed hooks produce output accepted by Cursor's native contract; install, reinstall and removal preserve unrelated user hooks; unsupported or unrepresentable behavior receives an explicit diagnostic without silent weakening or misleading success. Regression coverage checks native output and coexistence with Claude-hook import so corrected output does not newly double-activate hooks. Guidance states supported mappings and limitations.
    Out of scope: Invented or blanket case-converted event mappings, broadened matchers, claiming identical semantics across harnesses, silently skipping required behavior, a new target-specific hook-routing framework, the universal #2111 translation program, silently redirecting target scope, changing user import settings, or bypassing execution consent.
    Review contact: Daniel Meppiel (@danielmeppiel)
    Reason: Formatting-only republication of the unchanged human-approved scope at #3129 (comment); the original remains the rationale and context source. Recorded by Copilot on behalf of Daniel Meppiel (@danielmeppiel) following his explicit acceptance of these eight bounded scopes on 2026-10-02 and his subsequent instruction to start their delivery. No scope expansion, new product decision, assignment or merge approval.

  3. danielmeppiel commented on Oct 3, 2026

    @danielmeppiel
    Collaborator

    Decision: approve
    Area: project
    Scope: Additional specification and conformance work only for the already accepted Cursor-native hook installation, diagnostics, ownership/consent preservation and Claude-import coexistence behavior in PR #3149. Retain the functional scope and exclusions at #3129 (comment). Reuse genuinely applicable existing requirements first; add or clarify only the missing OpenAPM promises for that accepted behavior, following the existing spec-review and versioning process.
    Done when: Every changed promise has an accurate requirement mapping and real corresponding conformance assertions. Where normative text changes, update the canonical anchors, Appendix C, requirements manifest, affected schemas or fixtures as needed, marked tests and generated CONFORMANCE.md/CONFORMANCE.json together. Complete genuine required spec review and preserve evidence of actual execution. Do not invent citations or weaken the contract to fit current output; any production defect or broader work exposed by this exercise is reported and left blocked pending separate authorization.
    Out of scope: New targets, invented event mappings, broader matchers or hook-routing/translation frameworks beyond the original accepted scope; cached-plan production remediation or other exhausted general readiness/finalization retries under this addendum; apm-spec-waiver or new test waivers; weaker tests, relaxed LOC/ownership/consent budgets or detectors; reviewer bypass; merge. All original functional exclusions remain.
    Review contact: Daniel Meppiel (@danielmeppiel)
    Reason: Recorded by Copilot on behalf of Daniel Meppiel (@danielmeppiel) following his explicit selection: "Authorize bounded spec/conformance work (Recommended)". Published on 2026-10-03. The approval was for PR #3150 and PR #3149 only, limited to their already accepted native-compatibility behavior. The documented waiver applies to changes with no observable behavior delta; absence of a matching requirement is not itself that exception. This authorizes the bounded spec/conformance work unit through normal planning and authority gates, not a blanket restart of other exhausted remediation, a waiver, acceptance of non-executed reviews, or a merge.

  4. danielmeppiel commented on Oct 5, 2026

    @danielmeppiel
    Collaborator

    Decision: approve
    Area: project
    Scope: Resume end-to-end convergence of our existing PR #3149 for issue #3129 in convergence-20261005T122639Z. Deliver the accepted Cursor-native hook repair using verified vendor event, handler and matcher contracts; preserve user-owned hooks, APM ownership, executable consent and restrictions, and coexistence with native Claude-hook import without double activation. Fix and prove stale cached-preflight behavior within that accepted coexistence contract, including changed import state and plan use in another project; describing this gap as deferred is insufficient. Fold in-scope correctness, behavioral regression, conformance, guidance and finalization gaps, reconcile normative wording with verified supported behavior, and execute genuine current-head review through autopilot-pr-merge-worker and its composed skills. Necessary generated copies and their exact content-derived integrity hashes may be refreshed with complete accounting and no unrelated dependency or lock changes.
    Done when: Supported output satisfies the verified Cursor-native contract; install, reinstall and removal preserve unrelated hooks; unsupported or unrepresentable input receives an accurate diagnostic without weakening consent or claiming success. Actual behavioral assertions and mutation controls prove before-write coexistence checks remain valid despite stale cached state, and marked conformance tests execute the promised behavior rather than merely search prose. Genuine current-head/whole-conversation reviewer execution, canonical completion schema and owner semantic verification, current-main full local lint, exact-head functional/mutation evidence and clean/accounted locks are recorded. Required CI is complete and successful, live head/base are rechecked and mergeability is nonconflicting. After all review, CI, body and thread work, one new truthful ship_now advisory is verified as the latest top-level comment at that exact head, with any human branch-protection review gate disclosed. Preserve prior failures, blocked receipts and advisories as history; do not merge.
    Out of scope: Invented or blanket case-converted event mappings, broadened matchers, identical-semantics claims across harnesses, silently skipped required behavior, new target-specific routing frameworks, universal translation, changing user import settings or silently redirecting target scope; consent bypass, unrelated normative/product changes, dependency upgrades or unexplained lock churn. No contributor takeover, other PR work, fabricated reviewer/test evidence, relaxed checks or unauthorized waiver, human approval impersonation, reviewer bypass, auto-merge, merge, amend or published-history rewrite. This new run retains four outer iterations, two Copilot rounds and three CI recoveries; preserve closed attempts without resetting or rewriting their histories.
    Review contact: Daniel Meppiel (@danielmeppiel)
    Reason: Recorded by Copilot on behalf of Daniel Meppiel (@danielmeppiel) from the live human direction relayed by the product-lead session on 2026-10-05: "your goal is to keep looping until what we committed working on is in mergeable state with a ship_now comment as last comment", together with "allow everything, I need you to move forward fast and stop asking questions" for routine in-scope details. The parent explicitly included fixing the stale-preflight coexistence gap in the renewed bounded run. This record is the nominated current scope, supplementing original acceptance #3129 (comment) and superseding the closed spec-only run record #3129 (comment) for new execution; all earlier records and receipts remain intact. The authorized coordinator approves the exact plan before technical mutation. The strict record is evidence, not permission alone, and grants neither merging nor new product scope. Under the strict-record exception AI attribution stays in Reason; ordinary advisories keep their footer/watermark. This AI-generated transcription may contain errors.

  5. github-actions commented on Oct 6, 2026

    @github-actions

    Thanks for the detailed report. A maintainer-approved scope exists (Cursor-native hook output, coexistence with Cursor's Claude-hook import, no invented event mappings), and later records note that delivery is already underway in PR #3149. Docs and conformance guidance should state supported mappings and limits. No new recommendation beyond the recorded scope. This is automated advice only; it is not scope approval, an assignment or a release commitment.


    Generated by autopilot-issue-triage-worker. This comment is AI-generated and may contain errors.

    Generated by Triage Panel · copilot · auto · 75.3 AIC · ⌖ 7.84 AIC · ⊞ 12K · ◷

  6. added
    area/multi-targetMulti-target deploy spec, target directory creation, agent surface routing.
    triage/recommendedAutomated advice completed; not human scope approval.
    type/bugSomething does not work as documented.
    on Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area/multi-targetMulti-target deploy spec, target directory creation, agent surface routing.status/acceptedHuman scope approval; verify the issue's approval record and review contact before work.triage/recommendedAutomated advice completed; not human scope approval.type/bugSomething does not work as documented.

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions