--- source_keys: - apm-cli-installed-source - apm-docs-llms-full - claude-code-hooks-reference - github-copilot-hooks-configuration --- # 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 `stop` or a typo such as `PreToolUSe` never fires on Claude or Copilot), and a missing script only warns — so `apm install` exiting 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 `matcher` is 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 bash scripts/validate.sh ``` 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 IN` or `FILL_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.yml` at an inferred `.apm/` package root, or a `targets:` 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=value` assignments and `env` with its options, or the first operand after `bash`, `sh`, `zsh`, `python`, `python3`, `node`, `pwsh`, `ruby` or `perl`, past its options such as `-e` or `-u`; after a `sh`-family `-c`, the first token of the command string; inline code such as `python3 -c` has 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 `-c` command string (`bash -c "${PLUGIN_ROOT}/x.sh"`). Through an interpreter it is not. - Each event is judged per target the package root's `apm.yml` deploys to — no `target:`/`targets:`, `all`, or no `apm.yml` means 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's `apm.yml`, or beside a `plugin.json` at any location apm's `find_plugin_json` reads (root, `.github/plugin/`, `.claude-plugin/`, `.cursor-plugin/`) — passes. apm discovers both `.apm/hooks/` and `hooks/`, installs a Claude plugin with no `apm.yml`, and this audit may target a third-party package; `primitive-author` authors only in `.apm/hooks/`. Any other `hooks/` 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's `pre_run_command`) from a typo. A Cursor-only `stop` therefore exits 0. - Copilot counts every name apm's own Copilot map emits as fired (`userPromptSubmit`, although Copilot documents `userPromptSubmitted`): 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 `-c` string) is a FAIL, stricter than the research's Should, because it fails every time it fires. - `primitive-author` hook 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 own `targets:`, 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 numeric `timeout` in seconds. - SUGGESTION: a tool event (`PreToolUse`, `PostToolUse`) or `SessionStart` with no `matcher` — Claude receives `"*"`. A `matcher` on 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`/`timeoutSec` keys in a Claude-shaped file — they render, but leave stray keys in `settings.json`. - SUGGESTION: a helper `.json` file in the hook directory without a `hooks` key — 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: ```text Checked: structure · purpose · handlers ```