Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
fad0a34
fix(hooks): render native Cursor hooks with import coexistence checks
danielmeppiel Oct 2, 2026
b9b7af1
docs(hooks): align Cursor audit and refusal guidance
danielmeppiel Oct 2, 2026
41f5049
fix(hooks): reject unknown top-level keys in Cursor config + fix lint…
danielmeppiel Oct 2, 2026
4484bb8
fix(hooks): forward retiring_targets through fallback preflight path;…
danielmeppiel Oct 2, 2026
ae9aae3
fix(install): trim services.py below 1175-line LOC budget
danielmeppiel Oct 2, 2026
1b33db4
docs(spec): cite Cursor-native hook fail-closed validation and Claude…
danielmeppiel Oct 3, 2026
d3b2a84
Close cached-plan preflight bypass; harden req-tg-017 wording
danielmeppiel Oct 5, 2026
351b9fb
fix(#3129): close static-guard gap and harden reused-plan coverage
danielmeppiel Oct 5, 2026
ec13250
Close call-site lexical blind spot in neutral-hook-contract guard
danielmeppiel Oct 5, 2026
9de72a6
Apply ruff formatter to new call-site guard function
danielmeppiel Oct 5, 2026
9394a98
test: fold CLI-tier mutation proof, fixture and changelog fixes
danielmeppiel Oct 5, 2026
8544cf3
Clarify req-tg-017 ambiguities, forward refs, and add CLI-tier Windsu…
danielmeppiel Oct 5, 2026
0a8c183
Repair Cursor contraction coverage and native validation evidence
danielmeppiel Oct 6, 2026
8556bcc
Complete spec requirement cross-reference links
danielmeppiel Oct 6, 2026
bbc9093
Qualify Cursor diagnostic field naming guidance
danielmeppiel Oct 6, 2026
bade8f3
Name unsupported Cursor matcher-group fields
danielmeppiel Oct 6, 2026
13a741b
Clarify Cursor event-map vocabulary invariant
danielmeppiel Oct 6, 2026
1da8a09
Keep both fixes under one Unreleased changelog heading
danielmeppiel Oct 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .apm/architecture/owners/hooks-integrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"src/apm_cli/hook_contract.py",
"src/apm_cli/integration/hook_ir.py",
"src/apm_cli/integration/hook_native_formats.py",
"src/apm_cli/integration/hook_cursor_preflight.py",
"src/apm_cli/integration/hook_integrator.py",
"src/apm_cli/integration/hook_ownership.py"
],
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- Codex agent conversion preserves native `model` and `model_reasoning_effort` settings and warns about dropped metadata instead of silently losing it. (#3150)
- Cursor hooks use native v1 events and flat handlers; unsupported input and overlapping Claude imports now fail explicitly. Choose one hook route per dependency; see the [supported mappings](docs/src/content/docs/producer/author-primitives/hooks-and-commands.md#cursor-native-hooks-and-claude-import) and `openapm-v0.1.md` requirements `req-tg-016/017`. (#3149)

## [0.33.0] - 2026-10-02

Expand Down
32 changes: 29 additions & 3 deletions CONFORMANCE.json
Original file line number Diff line number Diff line change
Expand Up @@ -302,9 +302,10 @@
"keyword": "MUST",
"section": "5.2",
"status": "active",
"test_count": 2,
"test_count": 3,
"tests": [
"tests/spec_conformance/test_lockfile_reqs.py::test_cli_target_without_manifest_targets_preserves_inactive_state",
"tests/spec_conformance/test_lockfile_reqs.py::test_dropped_cursor_target_merge_hook_state_reconciled_fail_safe",
"tests/spec_conformance/test_lockfile_reqs.py::test_dropped_target_merge_hook_state_reconciled_fail_safe"
]
},
Expand Down Expand Up @@ -1555,12 +1556,37 @@
"tests": [
"tests/spec_conformance/test_manifest_reqs.py::test_codex_native_model_settings_preserved_and_dropped_metadata_bounded"
]
},
{
"conformance_class": "consumer",
"id": "req-tg-016",
"keyword": "MUST",
"section": "8.5.9",
"status": "active",
"test_count": 2,
"tests": [
"tests/spec_conformance/test_cursor_hook_reqs.py::test_cursor_native_fail_closed_conversion_clause_persists_in_spec",
"tests/spec_conformance/test_cursor_hook_reqs.py::test_cursor_native_fail_closed_conversion_is_real_not_prose"
]
},
{
"conformance_class": "consumer",
"id": "req-tg-017",
"keyword": "MUST",
"section": "8.5.9",
"status": "active",
"test_count": 3,
"tests": [
"tests/spec_conformance/test_cursor_hook_reqs.py::test_cursor_claude_import_overlap_rejection_clause_persists_in_spec",
"tests/spec_conformance/test_cursor_hook_reqs.py::test_cursor_claude_import_rejects_real_default_same_event_kind_content_overlap",
"tests/spec_conformance/test_cursor_hook_reqs.py::test_cursor_claude_overlap_predicate_is_kind_aware_not_event_only"
]
}
],
"spec_version": "v0.1.1",
"summary_by_class": {
"consumer": {
"active": 92,
"active": 94,
"skipped": 1,
"unbound": 0,
"xfail": 0
Expand All @@ -1584,5 +1610,5 @@
"xfail": 0
}
},
"total_requirements": 126
"total_requirements": 128
}
6 changes: 4 additions & 2 deletions CONFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ APM claims the proposed deployed-prompt-audit capability (req-pl-019, req-pl-020
| Class | Active | Skipped | Xfail | Unbound |
|-------|-------:|--------:|------:|--------:|
| Producer | 12 | 0 | 0 | 0 |
| Consumer | 92 | 1 | 0 | 0 |
| Consumer | 94 | 1 | 0 | 0 |
| Registry | 1 | 0 | 0 | 0 |
| Governance | 20 | 0 | 0 | 0 |

Expand Down Expand Up @@ -65,7 +65,7 @@ APM claims the proposed deployed-prompt-audit capability (req-pl-019, req-pl-020
| [req-lk-018](docs/src/content/docs/specs/openapm-v0.1.md#req-lk-018) | SHOULD | 5.5 | consumer | active | 1 | - |
| [req-lk-019](docs/src/content/docs/specs/openapm-v0.1.md#req-lk-019) | MUST | 5.2 | consumer | active | 1 | - |
| [req-lk-020](docs/src/content/docs/specs/openapm-v0.1.md#req-lk-020) | MUST | 5.2 | consumer | active | 3 | - |
| [req-lk-021](docs/src/content/docs/specs/openapm-v0.1.md#req-lk-021) | MUST | 5.2 | consumer | active | 2 | - |
| [req-lk-021](docs/src/content/docs/specs/openapm-v0.1.md#req-lk-021) | MUST | 5.2 | consumer | active | 3 | - |
| [req-lk-022](docs/src/content/docs/specs/openapm-v0.1.md#req-lk-022) | MUST | 5.2 | consumer | active | 6 | - |
| [req-mf-001](docs/src/content/docs/specs/openapm-v0.1.md#req-mf-001) | MUST | 4.1 | producer | active | 1 | - |
| [req-mf-002](docs/src/content/docs/specs/openapm-v0.1.md#req-mf-002) | MUST | 4.1 | producer | active | 1 | - |
Expand Down Expand Up @@ -167,6 +167,8 @@ APM claims the proposed deployed-prompt-audit capability (req-pl-019, req-pl-020
| [req-tg-013](docs/src/content/docs/specs/openapm-v0.1.md#req-tg-013) | MUST | 8.5.7 | consumer | active | 7 | - |
| [req-tg-014](docs/src/content/docs/specs/openapm-v0.1.md#req-tg-014) | MUST | 8.5.8 | consumer | active | 1 | - |
| [req-tg-015](docs/src/content/docs/specs/openapm-v0.1.md#req-tg-015) | MUST | 8.5.1 | consumer | active | 1 | - |
| [req-tg-016](docs/src/content/docs/specs/openapm-v0.1.md#req-tg-016) | MUST | 8.5.9 | consumer | active | 2 | - |
| [req-tg-017](docs/src/content/docs/specs/openapm-v0.1.md#req-tg-017) | MUST | 8.5.9 | consumer | active | 3 | - |

## Waivers

Expand Down
10 changes: 10 additions & 0 deletions docs/public/specs/manifests/openapm-v0.1.requirements.yml
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,16 @@ requirements:
section: "8.5.1"
conformance_class: consumer
notes: "a conforming consumer providing the Codex-native agent conversion capability preserves a source-declared model or model_reasoning_effort string value in its native top-level placement, leaves either field absent when absent from the source, diagnoses a non-string source value by field name without including that value, and emits a diagnostic bounded on dropped-field count (at least one shown when any are dropped), per-field name length, and ASCII-sanitized field-name display (never the value) for any other dropped, non-capability-restriction frontmatter field"
- id: req-tg-016
keyword: MUST
section: "8.5.9"
conformance_class: consumer
notes: "a Cursor-native hook installation implementation fails closed (no partial write) when converting a source-declared hook configuration whose event, top-level key, or handler field falls outside the accepted Cursor-native vocabulary, emitting an actionable diagnostic naming the unrecognized value(s)"
- id: req-tg-017
keyword: MUST
section: "8.5.9"
conformance_class: consumer
notes: "a Cursor-native hook installation implementation detects, before writing, an observable overlap with an in-effect Claude-settings hook import for either install order, and rejects the write with an actionable diagnostic rather than merging, redirecting, or broadening accepted input, without altering the Claude import's own settings"
- id: req-pr-006
keyword: MUST
section: "8.1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Claude (`PreToolUse`, `PostToolUse`) and Copilot (`preToolUse`,
| `Stop`, `AgentStop`, `agentStop` | `agentStop` | `Stop` |
| `UserPromptSubmit`, `userPromptSubmit`, `userPromptSubmitted` | `userPromptSubmitted` | `UserPromptSubmit` (native; the other two spellings are not renamed and will not fire) |

Event names absent from this table are preserved unchanged. Only an unmapped
For Copilot and Claude, event names absent from this table are preserved unchanged. Only an unmapped
camelCase or PascalCase name that conflicts with the target convention emits
an install warning. All-lowercase names such as `stop` pass through silently;
`stop` is not a native Copilot or Claude event and will not fire.
Expand Down Expand Up @@ -202,6 +202,84 @@ receives its current v1 standalone schema:
`{ "version": "v1", "hooks": [{ "name", "trigger", "matcher", "action" }] }`.
Kiro trigger names are PascalCase and command timeouts remain in seconds.

### Cursor native hooks and Claude import

Cursor requires `{"version": 1, "hooks": {...}}` with flat handlers.
APM accepts native Cursor event names and these documented Claude aliases:

| Claude source | Cursor native event |
|---------------|---------------------|
| `PreToolUse` | `preToolUse` |
| `PostToolUse` | `postToolUse` |
| `UserPromptSubmit` | `beforeSubmitPrompt` |
| `Stop` | `stop` |
| `SubagentStop` | `subagentStop` |
| `SessionStart` | `sessionStart` |
| `SessionEnd` | `sessionEnd` |
| `PreCompact` | `preCompact` |

For example, a Claude `PreToolUse` group with matcher `Bash` becomes:

```json
{
"version": 1,
"hooks": {
"preToolUse": [
{"type": "command", "command": ".cursor/hooks/my-package/hooks/check.sh", "matcher": "Shell", "timeout": 10}
]
}
}
```

Native Cursor spellings keep native matcher values unchanged. For the Claude
`PreToolUse` and `PostToolUse` aliases, APM translates literal tool alternatives:
`Bash` becomes `Shell`; `Read`, `Grep`, `Task`, `WebFetch`, and `WebSearch` keep
their names. `Edit|Write` becomes `Write`; either one alone is rejected because
Cursor combines both operations. Missing matchers and the unrestricted `""`,
`*`, and `.*` forms remain unrestricted.

APM rejects unknown events, other translated regexes, `Glob`, server-qualified
MCP matchers, and non-unrestricted matchers on other Claude aliases rather
than broadening their reach. It also rejects platform-specific commands,
unsupported handler fields/types, and invalid timeouts. Source documents may
contain `hooks`, optional `version: 1`, and a descriptive `description` string;
other source-level settings are rejected rather than silently discarded. Command and prompt
handlers retain native fields such as `failClosed`, `model`, and `loop_limit`.
Claude `Stop` and `SubagentStop` aliases get `loop_limit: null` to preserve the
documented unlimited import default rather than Cursor's native default of five.
Native Cursor hooks keep their own defaults.

Cursor's **Include Third-Party Plugins, Skills, and Other Configs** setting is
on by default. All matching native and Claude-imported hooks run; native hooks
do not replace imported hooks. APM therefore refuses overlapping native/import
actions before deploying the package's primitives. Checks cover planned
Claude/Cursor sources and existing project, project-local, and user hook
configs. "Owner" here means the installing dependency's own declared
provenance marker for that hook entry (recorded in the sidecar described
below). Same-owner events -- an event already attributed to the package
about to install or retain the other side's configuration -- are treated
conservatively as overlapping even when matchers differ; APM does not try
to prove arbitrary regexes disjoint.

Choose one hook deployment route per dependency. To reuse existing Claude
hooks, select `claude` for that dependency and verify third-party imports are
enabled in Cursor. To use native-only events such as `beforeReadFile`, author
Cursor-native hooks and select `cursor`. APM neither changes import settings
nor writes Claude configuration for a Cursor-only dependency. Different
scripts can still have overlapping behavior; test them in the actual harness.

Reinstall migrates matching APM-owned legacy Cursor entries and keeps unrelated
user hooks. Switching a dependency to one route can retire its old project
route; it does not remove user hooks or matching hooks in another scope.
Invalid or unsupported user configuration is rejected without being
overwritten. Target-specific files retain the existing filename-routing rules;
this repair does not add per-file target declarations or dry-run hook previews.

These mappings follow the [Cursor hook reference](https://cursor.com/docs/hooks)
and [third-party hook reference](https://cursor.com/docs/reference/third-party-hooks).
They are configuration compatibility, not a guarantee of identical execution
semantics. Cloud Cursor agents support command hooks only.

Copilot hook files are namespaced with the source package name to avoid
collisions across installed deps; bundled scripts land alongside under
`.github/hooks/scripts/<pkg>/`.
Expand Down Expand Up @@ -277,10 +355,15 @@ agent a procedure" fits a skill -- and reaches every harness.
## Pitfalls

- **Hook event names.** Use the documented
[session lifecycle aliases](#session-lifecycle-event-aliases). Unknown names
are preserved. An install warning appears only when an unmapped name starts
[session lifecycle aliases](#session-lifecycle-event-aliases) for Copilot
and Claude. Their unknown names are preserved. An install warning appears
only when an unmapped name starts
with a capital letter or starts lowercase and contains a later capital
letter, and that casing conflicts with the target convention.
letter, and that casing conflicts with the target convention. Cursor
instead rejects unsupported events, fields and restrictions; see
[Cursor native hooks](#cursor-native-hooks-and-claude-import). Diagnostics
name unsupported source-level, matcher-group and handler fields and identify
whitespace in literal matcher alternatives without silently stripping it.
- **Cursor command frontmatter loss.** Cursor reuses the Claude
command transformer today, so any prompt-only metadata is dropped
with a diagnostic. Keep Cursor commands to the preserved key set.
Expand Down Expand Up @@ -308,6 +391,7 @@ agent a procedure" fits a skill -- and reaches every harness.
a Claude+OpenCode package and assume hooks reach both -- they do
not. The install log notes the skip.

Once your hooks and commands are in place, run `apm install --dry-run`
to preview what each target will receive, then `apm pack` to bundle.
Once your hooks and commands are in place, inspect a real installation's
native configuration, then run `apm pack` to bundle.
`apm install --dry-run` does not render hook previews.
See [Compile](../../compile/) and [Pack a bundle](../../pack-a-bundle/).
2 changes: 1 addition & 1 deletion docs/src/content/docs/reference/cli/audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ not every file in a skill bundle.
|---|---|---|
| Copilot | `.github/instructions/*.instructions.md`, `prompts/*.prompt.md`, `agents/*.agent.md`, `.github/copilot-instructions.md`; shared skills | `.github/hooks/*.json`; commands are non-prompt |
| Claude | `.claude/rules/*.md`, `agents/*.md`, `commands/*.md`, `skills/**/SKILL.md` | `.claude/hooks/*.json`, `.claude/settings.json`; nested `prompt`/`agent` handlers' `prompt` field |
| Cursor | `.cursor/rules/*.mdc`, `agents/*.md`, `commands/*.md`; shared skills | `.cursor/hooks/*.json`, `.cursor/hooks.json`; flat or APM-preserved nested command handlers |
| Cursor | `.cursor/rules/*.mdc`, `agents/*.md`, `commands/*.md`; shared skills | `.cursor/hooks/*.json`, `.cursor/hooks.json`; flat command handlers and prompt handlers' `prompt` field; nested handlers are unsupported |
| Kiro | `.kiro/steering/*.md`, `agents/*.md`, `skills/**/SKILL.md` | `.kiro/hooks/*.json`; v1 `hooks[].action.prompt` for agent actions |
| Gemini | `.gemini/commands/*.toml`: `prompt`; shared skills | `.gemini/hooks/*.json`, `.gemini/settings.json`; nested command handlers |
| Codex | `.codex/agents/*.toml`: `developer_instructions`; shared skills | `.codex/hooks.json`; nested command handlers |
Expand Down
5 changes: 5 additions & 0 deletions docs/src/content/docs/reference/common-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ APM v0.21.0+ always writes `"version": 1` to `.cursor/hooks.json` on a
fresh install. Existing files that already contain a `"version"` key are
left untouched.

If installation instead rejects unsupported Cursor events or reports overlap
with Claude import, follow the
[native hook compatibility and one-route guidance](../../producer/author-primitives/hooks-and-commands/#cursor-native-hooks-and-claude-import).
APM does not overwrite invalid user hooks or change Cursor's import settings.

---

### Agent Plugin does not load in Copilot
Expand Down
Loading
Loading