- factory-audit: ./ and bare/absolute script checks scoped to command position (no false FAILs on ./src or printf); hook sources limited to .apm/hooks or package-root hooks/; Kiro-aware lowercase events; unfilled template placeholders FAIL; repo-only instructions FAIL at any scope; Vale description FAIL documented; bats 367 -> 378 - primitive-author: split-quote/spaced paths and handler-less entries promoted to Must; Step 4.2 renders into a scratch consumer instead of a no-op dry run; dispatch and gate hand-off trimmed - apm-workflow 1.0.2: mutual boundary with primitive-author - forge: no double package bump; gotcha wording - skill-author: create keeps seeded 0.1.0 (ADR-0022); portable, retry-safe new-skill.sh; template and flow consistency fixes - hook docs: cite the ADR-0019 correction; guard caveat Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
5.6 KiB
5.6 KiB
source_keys
| source_keys | ||
|---|---|---|
|
Authoring an apm hook
Reached from SKILL.md Step 1 for a hook. Run the Gate, then write against the shape and the
checklist, then return to SKILL.md Step 3.
Gate
A hook is a runtime callback the harness fires inside its own tool loop — "this must always happen at this event", enforced deterministically rather than left to the model. apm's own guidance is to reach for a skill, instruction or prompt first, and to treat hooks as opt-in surface: they ship to a strict subset of harnesses and are silently skipped everywhere else.
- Procedure, know-how, or anything the model should decide to do → a skill. Stop and hand to
skill-author. - The hook must reach only some harnesses → reach is set by the package
apm.ymltargets:, never by the hook file. Stop and hand toapm-workflow.targets:is package-wide, so a harness-specific hook in a multi-target package means either a separate package or accepting that the other targets receive it too. - A runtime callback → continue.
Shape
One JSON file per concern at .apm/hooks/<name>.json, with a plain name. Copy
assets/templates/hook.json.template. Write the canonical shape apm documents and renders per
target:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{"type": "command", "command": "${PLUGIN_ROOT}/.apm/hooks/check.sh", "timeout": 10}
]
}
]
}
}
${PLUGIN_ROOT}is the target-neutral token; apm rewrites it per target ("${CLAUDE_PROJECT_DIR}/.claude/hooks/<pkg>/…"on Claude, repo-relative elsewhere).${CLAUDE_PLUGIN_ROOT}is rewritten identically, so it is valid, but it ties the source to one harness's name (Should 11).- Claude is the verified target. apm 0.28.0 passes this nested shape to Copilot without
reshaping it, and whether Copilot CLI runs nested entries or honours
matcheris unverified. That gap is apm's to close. Per-file target routing is deprecated, so a Copilot-native flat hook (bash/powershell/timeoutSec) can only live in a separate Copilot-targeted package — hand that toapm-workflowrather than adding a second file here.
Checklist
Must:
- The file sits directly in
.apm/hooks/, is not a symlink, and parses as a JSON object. apm skips invalid JSON silently. (apm also discovers a package-roothooks/, andfactory-auditaccepts it for third-party packages; author in.apm/hooks/.) - Use the wrapped shape
{"hooks": {Event: [...]}}. If a naked settings slice is used instead, every top-level value must be a list, with no stray scalar keys anywhere. - Every event value is a list of objects, and every nested
hooksis a list of objects. Anything else fails the Copilot install outright. The file contributes at least one entry, and every entry carries at least one handler: an empty list or a handler-less entry deploys nothing, with only a warning. - Event names are PascalCase (
PreToolUse,PostToolUse,UserPromptSubmit,SessionStart,Stop, …). An all-lowercase name never warns: only Kiro's rename map covers one (stop→Stop), and every other target deploys it verbatim, where it never fires. A camelCase name outside apm's rename map (userPromptSubmit) likewise deploys verbatim to Claude and never fires. - The script is referenced as
${PLUGIN_ROOT}/…(or${CLAUDE_PLUGIN_ROOT}/…, see Shape) for the package root, or./…for the hook directory, and exists inside the package. The script is the command's first token or the first argument after an interpreter (bash,sh,zsh,python,python3,node,pwsh,ruby,perl); in either position, no absolute path and no bare relative path (scripts/x.sh): apm bundles and rewrites neither. No$or backtick in the path itself, and no space. When quoting, quote the whole token —"${PLUGIN_ROOT}/scripts/my-hook.sh", never"${PLUGIN_ROOT}"/scripts/x.sh: apm rewrites${PLUGIN_ROOT}only when a path separator follows it directly, and only up to the next space or quote, so a split quote is left unrewritten and a spaced path is cut short. This is stricter than the research's Should, as with Must 6: either defect fails every time the hook fires. A missing script is only a warning at install time. - A script run directly as the command's first token is executable. This is stricter than the
research's Should: without it the hook fails every time it fires. A script passed to an
interpreter (
bash ${PLUGIN_ROOT}/x.sh) needs no executable bit.
Should:
- Every handler sets
"type": "command"and an explicittimeoutin seconds — apm passestypethrough but never supplies it. - Set
matcherexplicitly on tool events and onSessionStart(startup,resume, …). Omitted, Claude receives"*". - Do not author Copilot's flat
bash/powershell/timeoutSeckeys in a Claude-shaped file; they render onto Claude as stray keys. - Keep helper files in the hook directory non-JSON. Copilot's loader rejects any bundled
.jsonwithout ahookskey. - Prefer
${PLUGIN_ROOT}over${CLAUDE_PLUGIN_ROOT}. - No filename that routes by target. Case-insensitively, apm routes a stem of exactly
hooks-<target>and any stem ending<target>-hooks— bare (claude-hooks), prefixed (x-claude-hooks) or combined (claude-codex-hooks, the union). That routing is deprecated and reach belongs totargets:(see Gate); the research allows it only when deprecated routing is intended.