Why: PR #144 review round 4 reproduced hooks referencing $PLUGIN_ROOT or ${PLUGIN_ROOT} without a path separator passing the audit, although apm only rewrites ${TOKEN}/ and the deployed hook points nowhere. - FAIL unbraced or unseparated plugin-root tokens - check the exec bit for scripts run via an interpreter -c string - skip env NAME=value prefixes when locating bare relative paths - correct input: and empty-frontmatter messages, depth-walk applyTo braces - INFO on unrecognised targets; failing-case tests for untested checks - document tiers, blind spots and crash exit 2; drop rtk from portable flow - restore the after-a-hand-edit trigger; pin upstream apm source URL Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
8.7 KiB
source_keys
| source_keys | ||||
|---|---|---|---|---|
|
Hook Flow
Steps 1 to 3 for an apm hook — the target Step 0 matched as a .json file directly under a
hooks/ directory. Work them in order, then return to SKILL.md Step 4 to report.
Gotchas
- apm checks almost nothing here. Invalid JSON is skipped without a word, an event name its rename map does not cover deploys verbatim with no warning (a lowercase
stopor a typo such asPreToolUSenever fires on Claude or Copilot), and a missing script only warns — soapm installexiting 0 says nothing about whether the hook works. Never cite a clean install as evidence against a finding. - Copilot receiving a Claude-shaped file is not a finding. apm renders one source for every target and documents that it owns the per-target shape; whether Copilot CLI honours a nested entry or
matcheris unverified upstream, not a defect in the file. - Quoting is not the fix for a script path with a space. apm rewrites a whole-token-quoted
"${PLUGIN_ROOT}/scripts/x.sh", but still stops reading the path at the space; the only fix is a path without one.
Step 1 — Deterministic checks
Resolve the path against this skill's own directory. Run exactly:
bash scripts/validate.sh <hook-file>
Its findings become the ### Structure dimension, FAILs and SUGGESTIONs both, at the tier the script assigned.
Checks:
- JSON validity and UTF-8 encoding; a top level that is not a JSON object; the wrapped-or-naked shape; event lists and nested handler lists (the checks whose failure makes the Copilot install fail).
- A file contributing no entries (no events, only empty event lists, or an entry with no handler), an empty event name, and event names that never fire.
- Unfilled
FILL INorFILL_IN_template placeholders. - A symlinked file, or one under apm's deployed output or outside any package rather than package source (exit 1, a finding, not exit 2).
- Referenced scripts that are missing, outside the package, not executable when run directly, or referenced by an absolute, bare relative,
../, split-quoted, space-containing, or$/backtick-containing path apm will not bundle correctly. - A plugin-root token apm never rewrites: unbraced (
$PLUGIN_ROOT/x.sh,$CLAUDE_PLUGIN_ROOT), or braced but not directly followed by/or\(cd ${PLUGIN_ROOT} && …). - Deprecated filename routing, and
${CLAUDE_PLUGIN_ROOT}where${PLUGIN_ROOT}would do. - INFO: no
apm.ymlat an inferred.apm/package root, or atargets:naming no hook target apm recognises (claude-code), which leaves event names checked against no harness.
Script reference rules:
- Script references are read with apm 0.28.0's own patterns:
${PLUGIN_ROOT}/…only when the path follows the token directly, up to the first whitespace or quote, and./…anywhere in the command. - A
./or../match is held to the script rules — a FAIL when missing — only in command position, when it ends in a script extension, or when it names a package entry that is not a file; any other match (npx prettier --check ./src,printf '.\n') is at most a SUGGESTION, because apm only warns and it runs against the consumer's working directory as meant. - Command position is the first token past any
NAME=valueassignments andenvwith its options, or the first operand afterbash,sh,zsh,python,python3,node,pwsh,rubyorperl, past its options such as-eor-u; after ash-family-c, the first token of the command string; inline code such aspython3 -chas none. Absolute and bare relative script paths are checked in the same positions. - The exec bit is required of a script that runs directly: the first token, or the first token of a
-ccommand string (bash -c "${PLUGIN_ROOT}/x.sh"). Through an interpreter it is not. - Each event is judged per target the package root's
apm.ymldeploys to — notarget:/targets:,all, or noapm.ymlmeans every hook target — after apm's rename map for that target: it FAILs when a target with a published event list (Claude, Copilot) does not fire the renamed name, whatever its casing.
Exit codes: 0 with no FAIL, 1 on real findings, 2 when it never ran, including a crash inside the checks — report that as ### Structure unverified, quoting the stderr reason. An INFO line is observational: report it under ### Structure and count it in · P info.
The command parser is a heuristic, not a shell. Known blind spots: a script after &&, ; or a pipe inside one command, env -S, command substitution, and a quoted -c string that ends before the script are not in command position, so a bad reference there is at most a SUGGESTION or unseen. Read every command in Step 2 rather than taking a clean script run as proof.
There is no provenance and no Vale step: a hook carries no source_keys and no prose.
Six tiers deliberately differ from primitive-author's checklist or the research's. Do not re-tier them by judgment:
- A hook file directly under a package-root
hooks/— beside the package'sapm.yml, or beside aplugin.jsonat any location apm'sfind_plugin_jsonreads (root,.github/plugin/,.claude-plugin/,.cursor-plugin/) — passes. apm discovers both.apm/hooks/andhooks/, installs a Claude plugin with noapm.yml, and this audit may target a third-party package;primitive-authorauthors only in.apm/hooks/. Any otherhooks/directory (.github/hooks/,.cursor/hooks/, …) is apm's deployed output, or no package source at all, and FAILs at exit 1. - An event that every listed target fires, but that reaches a target with no published event list (Cursor, Kiro, Gemini, Codex, Antigravity, Windsurf) in a non-PascalCase form after apm's rename, is a SUGGESTION, not the FAIL Must 4 implies: the script cannot tell a harness's native spelling (Cursor's
stop, Windsurf'spre_run_command) from a typo. A Cursor-onlystoptherefore exits 0. - Copilot counts every name apm's own Copilot map emits as fired (
userPromptSubmit, although Copilot documentsuserPromptSubmitted): the author cannot route around apm's rename, so that is not a finding in the file. - Deprecated filename routing is a SUGGESTION, matching the author's Should: the research allows it when deprecated routing is intended.
- A non-executable script run directly (the first token, or first in a
-cstring) is a FAIL, stricter than the research's Should, because it fails every time it fires. primitive-authorhook Must 5 bans an absolute or bare relative path "in any position"; this audit checks command positions only, because a later argument is data the script cannot tell from a path. Judge the rest by reading in Step 3.
Step 2 — Read the hook and its scripts
Read the hook file, every script it references, and the package's apm.yml targets: — reach is narrowed there, never in the hook file.
Step 3 — Qualitative audit
Cite file and line for every finding.
purpose — apm's own rule is to reach for a skill, instruction or prompt first; a hook is for "this must always happen at this event".
- FAIL: the script carries procedure the agent should follow — instructions printed to the model, a multi-step workflow — rather than a runtime callback. That is a skill.
- SUGGESTION: the behaviour is harness-specific (a Claude-only event reaching a harness the script has no event list for, a Claude-only matcher value) in a package whose
targets:includes other harnesses, and nothing records that the other targets receiving it was accepted. The apm-native fix is a separate package with its owntargets:, not a routing filename.
handlers — the research checklist's Should and audit-only items, which apm never checks:
- SUGGESTION: a handler without
"type": "command"or an explicit numerictimeoutin seconds. - SUGGESTION: a tool event (
PreToolUse,PostToolUse) orSessionStartwith nomatcher— Claude receives"*". Amatcheron an event Claude ignores it for (Stop,UserPromptSubmit) is inert, not wrong. - SUGGESTION: a PascalCase event, in a package reaching only harnesses with no published event list, that is not one of that harness's events — the script FAILs a misspelling only where it has the list (Claude, Copilot).
- SUGGESTION:
bash/powershell/timeoutSeckeys in a Claude-shaped file — they render, but leave stray keys insettings.json. - SUGGESTION: a helper
.jsonfile in the hook directory without ahookskey — Copilot's loader scans the bundled scripts directory and rejects it. Keep helper configuration non-JSON.
Then return to SKILL.md Step 4, opening the report with this coverage line:
Checked: structure · purpose · handlers