Repository navigation
fix(hooks): type untyped flat command handlers for Claude - #3153
Daniel Meppiel (danielmeppiel) merged 10 commits into
Conversation
Flat hook command entries that omit `type` (valid in Cursor, where it defaults to `command`) were wrapped into Claude's matcher groups without the handler `type` field that Claude's hook schema requires. The Claude renderer now supplies `"type": "command"` for flat entries that the neutral hook grammar reads as command handlers. Explicit handler types, untyped handlers already inside nested Claude groups, non-command entries, and all other targets are unchanged. Reinstall cleanup also matches the pre-fix untyped render, so stale root-package entries keep healing instead of duplicating (the microsoft#1329 / microsoft#1392 contract), mirroring the existing Codex legacy-content-key path. Refs microsoft#3130 apm-spec-waiver: Claude-native handler type default for microsoft#3130; OpenAPM v0.1 defines no target-native hook handler fields, the portable hook subset is deferred to microsoft#2111 (out of approved scope), and req-lk-021 ownership reconciliation is preserved.
|
@microsoft-github-policy-service agree |
|
Hi Daniel Meppiel (@danielmeppiel), one scope question carried over from #3130: This PR adds type: "command" only to flat untyped command entries, both standalone and merged into The approved scope says "recognized command handlers", which could cover these too. Done-when only I'm also happy to mark this ready for review whenever it suits you, so CI can run. |
Mohammed Alkindi (MohammedAlkindi)
left a comment
There was a problem hiding this comment.
The codex case in test_claude_handler_default_does_not_reach_other_targets asserts '"type"' not in rendered, but Codex needs that field too: MatcherGroup.hooks is Vec<HookHandlerConfig>, an enum with #[serde(tag = "type")] (codex-rs/config/src/hook_config.rs in openai/codex), so an untyped handler fails to deserialize. Leaving Codex alone fits the approved scope, but this assertion pins the invalid output. Limiting the parametrization to cursor keeps the scope check without blocking a Codex follow-up.
Preserve current main changes while validating the bounded microsoft#3130 fix. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f79e464c-180e-4be3-9056-0773aa679a1a
APM review: current-main technical readinessPublished head The scope remains recognized untyped flat Claude command entries only. Explicit types, nested groups, noncommands, other targets, ownership and consent are unchanged relative to current main. Faithful integration, not a fresh panelBoth complete patches - this PR's changes and main's changes - are content-identical after integration apart from diff metadata. All 73 main-only files match main exactly, including landed hook-path admission and build provenance. Only the adjacent Unreleased entry overlap needed resolution; all entries and released changelog bytes are preserved. The genuine architect, coverage and CEO review at Current-head evidence
Thanks parth (@Parth-Vasave) for accepting the folds. Contributor history is preserved; no author rebase or extra push is requested. Original 2/0/0 counters and returns are unchanged; this separately recorded compatibility phase used 1 outer / 0 Copilot / 0 CI recovery. The worker did not promote the draft, merge, assign, request reviewers or alter issue state. Publication stays with the maintainer. Generated by autopilot-pr-merge-worker. This comment is AI-generated and may contain errors. |
Fold panel coverage gaps for microsoft#3130: verify ownership sidecars, actual CLI install/reinstall and legacy root healing without executing hooks. Preserve non-target behavior without pinning invalid Codex output; add canonical-edge and explicit-type traps and clarify flat-only guidance. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f79e464c-180e-4be3-9056-0773aa679a1a
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f79e464c-180e-4be3-9056-0773aa679a1a
Keep the integrator below the canonical module limit by consolidating the duplicate legacy-key selection through the existing native renderer owner. Preserve Codex flat legacy matching and Claude untyped upgrade healing. Add a static boundary assertion and mutation-proven functional guardrail. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f79e464c-180e-4be3-9056-0773aa679a1a
|
Thanks Daniel Meppiel (@danielmeppiel) for the panel pass, and Mohammed Alkindi (@MohammedAlkindi) for the Codex catch. I'll take the follow-ups myself in this PR:
Per the panel, nested-group defaults stay out of scope, so the PR remains flat-input only. I'll also rebase onto current |
|
Thanks Daniel Meppiel (@danielmeppiel), your convergence pass covered all three follow-ups, including a cleaner fix for Mohammed Alkindi (@MohammedAlkindi)'s Codex point, so I've dropped my local version and won't push over it. Happy to help with anything else before this comes out of draft. |
Preserve the reviewed flat-only Claude repair and all landed main changes. The automatic merge retains both changelog entries and independent hook architecture guard extensions without changing regression tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f79e464c-180e-4be3-9056-0773aa679a1a
Preserve reviewed flat-only behavior and contributor ancestry while keeping the undelivered fix under Unreleased. Retain main v0.34.0 and its registry-delegation mutation case. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f79e464c-180e-4be3-9056-0773aa679a1a
Preserve both native-renderer improvements and both sets of regression tests. Resolve only the adjacent test insertion and Unreleased entry conflict; retain the dated release unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f79e464c-180e-4be3-9056-0773aa679a1a
Preserve the unchanged Claude-only patch and all landed hook-package admission, build provenance, marketplace and Codex improvements. Resolve only the adjacent Unreleased entry overlap; retain released changelog bytes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f79e464c-180e-4be3-9056-0773aa679a1a
a30aeb3
into
microsoft:main
There was a problem hiding this comment.
🟢 Approval recommended
The bounded rendering change preserves compatibility and ownership safeguards, with focused regression coverage and no unresolved findings.
No open issues 🎉
What changed in this PR
Fixes #3130 by adding Claude's required "type": "command" to untyped flat command hooks while preserving explicit types, nested handlers, and other targets.
Changes:
- Adds Claude-only handler typing and legacy-format matching for reinstall cleanup.
- Adds rendering, ownership, upgrade, and architecture-guard regression coverage.
- Updates authoring guidance and the changelog.
| File | Description |
|---|---|
tests/unit/integration/test_hook_integrator_issue3130.py |
Covers typing, preservation, target isolation, and legacy cleanup. |
tests/integration/test_required_lifecycle_state_machine.py |
Tests real CLI upgrades and repeated-install convergence. |
tests/integration/test_architecture_contract_guards.py |
Tests default typing and legacy-selector enforcement. |
src/apm_cli/integration/hook_native_formats.py |
Adds Claude typing and shared legacy-shape selection. |
src/apm_cli/integration/hook_integrator.py |
Uses legacy shapes for ownership matching. |
scripts/architecture_linter/checks/mutation_hook_contract.py |
Guards delegation to the legacy-shape selector. |
packages/apm-guide/.apm/skills/apm-usage/package-authoring.md |
Documents the flat-only default in packaged guidance. |
docs/src/content/docs/producer/author-primitives/hooks-and-commands.md |
Clarifies Claude handler typing behavior. |
CHANGELOG.md |
Records the fix under Unreleased. |
Review effort: Balanced
fix(hooks): type untyped flat command handlers for Claude
Description
Claude-native rendering now supplies
"type": "command"for flat hook command entries that omittype, so.claude/settings.jsonreceives schema-valid handlers. Explicit handler types, nested Claude groups, non-command entries and every other target are unchanged.TL;DR
Flat hook entries without
typeare valid in Cursor (wheretypedefaults tocommand), but the Claude render wrapped them into{ "matcher", "hooks": [...] }groups and left the handler without thetypefield Claude's hook schema requires. The Claude renderer now adds it for recognized command entries only. A shared legacy-native selector keeps reinstall cleanup recognizing entries written before this fix without changing ownership or consent policy.Note
Flat input only. Nested Claude handlers remain as authored. Released main is integrated and its previously missing owner-mutation case now passes. The dated 0.34.0 changelog is unchanged; this undelivered fix remains under Unreleased. Technical readiness does not authorize draft publication. The contributor accepted the completed folds; no author rebase or additional push is requested. This PR remains draft.
Problem (WHY)
preToolUsewithcommand,matcher,timeout, notype) rendered a Claude handler with notype. As the report puts it: "The nesting happens, but the handler is incomplete."PreToolUselist, still untyped..apm/hooksentries carrying a stale source marker would be left behind as an untyped duplicate instead of healed, regressing the#1329/#1392contract.The approved Done-when requires schema-valid Claude command handlers from applicable untyped flat input while preserving ownership and other targets. This PR claims schema validity, not a demonstrated Claude Code runtime rejection. The regression and mutation evidence below makes that boundary checkable, following Agent Skills' validation pattern: "do the work, run a validator (a script, a reference checklist, or a self-check), fix any issues, and repeat until validation passes."
Approach (WHAT)
type: "command"for a flat entry with notypethat the neutral hook grammar reads as a command (command, orbash/powershell/windowsnormalized tocommand).promptwith acommandfield), nested Claude groups, and untyped non-command entries as authored.hook_contract.py,hook_ir.py) and other targets' rendering behavior unchanged; Codex's existing typing and event mapping are not overridden._legacy_hook_entries(): pre-default Claude groups, existing flat Codex entries, and no legacy entries for other targets.Implementation (HOW)
src/apm_cli/integration/hook_native_formats.py_with_claude_default_handler_type()reuses_handler_to_ir()for command recognition._to_claude_hook_entries(..., default_handler_type=False)reproduces pre-fix output;_legacy_hook_entries()owns Claude/Codex legacy-shape selection.src/apm_cli/integration/hook_integrator.pylegacy_content_keys, replacing the inline Codex-only branch. Existing ownership removal policy is unchanged. The file is 2,099 of 2,100 lines at this head.scripts/architecture_linter/checks/mutation_hook_contract.py_nhc_legacy_native_shapesundermutation_writes.neutral_hook_contract, requiring cleanup to call the native-shape owner.tests/unit/integration/test_hook_integrator_issue3130.pytests/integration/test_architecture_contract_guards.pytests/integration/test_required_lifecycle_state_machine.pydocs/src/content/docs/producer/author-primitives/hooks-and-commands.mdpackages/apm-guide/.apm/skills/apm-usage/package-authoring.mdCHANGELOG.mdFixedentry under[Unreleased], crediting parth (@Parth-Vasave) and linking #3153 / #3130.Diagrams
Legend: dashed nodes are new native-renderer helpers; legacy and current Claude forms meet at existing ownership reconciliation before settings and sidecar writes.
flowchart LR subgraph Source[Source] A["Flat Claude command entries"] end subgraph Render[Native shapes] L["_legacy_hook_entries: pre-fix form"] N["_with_claude_default_handler_type: recognized untyped flat entries"] C["_to_claude_hook_entries: nested groups"] end subgraph Reconcile[Ownership reconciliation] K["_hook_entry_content_key: compare old and current forms"] W["Write settings.json and apm-hooks.json"] end A --> L L --> K A --> N N --> C C --> K K --> W classDef new stroke-dasharray: 5 5; class L,N new;Trade-offs
#2111remains separate. Nested-metadata rendering is now delivered on main and is retained here without claiming that issue's scope.Benefits
{"type": "command", "command": "...", "timeout": 10}instead of an untyped handler.settings.jsonandapm-hooks.json.command/prompttypes, nested handlers, and non-command entries retain their existing behavior.Historical contributor observations (not current-head assertions)
The original write-up recorded these observations around
cecf16be/18c4c43c(v0.33.0). They were not revalidated as part of this body correction and are not evidence of failures at the reviewed head:apm.lock.yaml,.github/mcp.json,.codex/config.toml,apm_modules/,.local/state/gh/andbuild/lib/into the checkout.uv run pytestwas reported to skip the suite through the performance opt-in collection hook; the v0.33.0 lockfile was reported to reference the Microsoft package feed.pwshfailure.Issue and approved scope
Issue: Fixes #3130
Human scope-approval comment: #3130 (comment)
This PR completes the bounded issue scope: Claude-native typing of untyped flat command handlers (standalone and merged alongside an existing Claude-shaped hook file), preserved explicit and non-command types, regression coverage for both cases, and related guidance. It does not change shared hook representation, other targets' behavior, Codex event-name mapping, ownership or consent behavior. Contributor coordination is resolved by acceptance of the folds; the existing advisory is
ship_nowfor the reviewed head, not an instruction to publish or merge.Type of change
Testing
Evidence below is bound to
832e19a9ff269abb9eddb12e319934923928a6c1, with tested base897496177ba389c4708b110efa1ee5345ccefb79. "All existing tests pass" remains unchecked because no full local suite was run. This PR remains open and draft; no draft promotion, merge, assignment or reviewer request is authorized by technical success.Validation evidence
bash .worker-evidence/lint.shranked-rolling-20261020/lint.log.registry-delegation-build-id-verificationmutation caseranked-rolling-20261020/owner-verification-technical.jsonandjunit-mappings.json. The verifier took its evidence-required path, not the blocked-status exemption.bbd89bf3e4ea5ff9ad59ea86f4e34a9203285b77carried forward with byte/AST proof of PR-owned additions and faithful main integration; no new panel or original mutation-program execution is claimed.3241b81d54ce68184a99a2b8f6bb95a6558ec494, including the full owner mutation file. Preserved as prior-head evidence, not re-labelled as current-head execution.bbd89bf3e4ea5ff9ad59ea86f4e34a9203285b77. Not new local runs. Current remote conformance/docs succeeded.Standard technical workflows at this head: CodeQL, Spec conformance, NOTICE Drift Check, Deploy Docs, CI, Merge Gate, all success. This includes actual CI unit shards, lifecycle and Windows jobs, not just compliance checks.
gh pr checks 3153 --repo microsoft/apm --watch --interval 30exited 0. First-time-contributor execution permission covered only these existing reviewed-head workflows.Exact current-head command and retained review provenance
Run from the isolated worktree.
.worker-evidenceholds retained worker artifacts, not committed project files.The script runs CI's exact frozen ruff check/format and pylint R0801 commands over
src/,tests/and the required architecture/Windows entrypoints; macOS-compatible equivalents cover YAML I/O, portable relative paths and the 2,100-line limit, followed by auth and architecture boundaries.The existing five mutation programs at
bbd89bf3e4ea5ff9ad59ea86f4e34a9203285b77produced 10, 3, 1, 1 and 4 expected test failures respectively, then restored the guards. They cover default typing, legacy cleanup, explicit type preservation, the original rewrite-scope check, and paired legacy-owner/static-guard removal. These are failed-test counts, not numbers of programs. PR-added helpers, regression traps and guards retain their AST. Main's delivered metadata renderers, guards and regression tests are retained unchanged alongside them; all current-main hook-path admission and build-provenance changes are also retained. Only the adjacent Unreleased entry overlap required resolution. The retained earlier proofs andranked-rolling-20261020/provenance-final.jsonestablish the chain. The complete PR and main patches are identical apart from diff metadata, and 73 main-only files are byte-identical. Original contributor commits remain ancestors.Scenario Evidence
tests/unit/integration/test_hook_integrator_issue3130.py::test_standalone_flat_untyped_entry_gets_command_type(regression-trap for #3130)tests/unit/integration/test_hook_integrator_issue3130.py::test_flat_untyped_entry_merged_next_to_claude_shaped_filetests/unit/integration/test_hook_integrator_issue3130.py::test_flat_handler_types_are_defaulted_only_for_commands(7 cases);tests/unit/integration/test_hook_integrator_issue3130.py::test_untyped_handler_inside_nested_claude_group_is_unchangedtests/unit/integration/test_hook_integrator_issue3130.py::test_claude_handler_default_does_not_reach_other_targets(2 cases; helper-isolation check)tests/unit/integration/test_hook_integrator_issue3130.py::test_reinstall_replaces_untyped_group_written_before_fix;tests/unit/integration/test_hook_integrator_issue3130.py::test_stale_root_source_untyped_group_is_still_healedtests/integration/test_required_lifecycle_state_machine.py::test_required_claude_untyped_hook_upgrade_preserves_ownership_and_converges[standalone]and[merged]The registered owner guard is separately proven by
tests/integration/test_architecture_contract_guards.py::test_legacy_hook_shapes_boundary_rejects_cleanup_bypass; it and both CLI mappings passed at832e19a9ff269abb9eddb12e319934923928a6c1within the 340-test run, with no skips.How to test
uv run --frozen --extra dev pytest -q tests/unit/integration/test_hook_integrator_issue3130.py; expect 14 passed..venv/bin/apm; expect both standalone/merged CLI upgrade scenarios and owner-boundary tests to pass.preToolUsehook withouttypeusingapm install --target claude; confirmPreToolUsein.claude/settings.jsonhas"type": "command"and.claude/apm-hooks.jsonrecords the owned group.uv run --frozen --extra dev pytest -q tests/unit/integration/test_hook_integrator_issue3130.py -k 'other_targets or nested or defaulted_only'; confirm other-target isolation and explicit/nested/non-command preservation.Spec conformance (OpenAPM v0.1)
If this PR changes behaviour that an OpenAPM v0.1
req-XXXcovers,confirm the three-step ritual in the
development guide:
docs/src/content/docs/specs/openapm-v0.1.mdupdated(new/changed
<a id="req-XXX"></a>anchor + prose + Appendix Crow).
docs/src/content/docs/specs/manifests/openapm-v0.1.requirements.ymlupdated.
@pytest.mark.req("req-XXX")test undertests/spec_conformance/added or extended.CONFORMANCE.{md,json}regenerated viauv run --extra dev python -m tests.spec_conformance.gen_statementand committed.
src/apm_cli/integration/is a Mode B critical path; the reviewed diff has 25 substantive added lines there. The unmodified Mode B script accepted the existing contributor waiver. Noreq-XXXcovers target-native hook handler fields, andreq-lk-021ownership reconciliation is preserved, not changed.apm-spec-waiver: Claude-native hook handler type default; OpenAPM v0.1 defines no target-native hook handler fields and req-lk-021 ownership reconciliation is preserved
Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com