Files
holocron/plugins/kyberforge/.apm/skills/factory-audit/references/hook-flow.md
Defame1297 9285b29e3c fix(factory-audit): flag unbraced plugin-root tokens and close hook check gaps
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
2026-09-29 08:00:17 +00:00

8.7 KiB

source_keys
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 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 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:

Checked: structure · purpose · handlers