--- source_keys: - apm-cli-installed-source - apm-docs-llms-full --- # 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.yml` `targets:`, never by the hook file. Stop and hand to `apm-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/.json`, with a plain name. Copy `assets/templates/hook.json.template`. Write the canonical shape apm documents and renders per target: ```json { "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//…"` 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 12). - **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 `matcher` is 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 to `apm-workflow` rather than adding a second file here. ## Checklist Must: 1. 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-root `hooks/`, and `factory-audit` accepts it for third-party packages; author in `.apm/hooks/`.) 2. 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. 3. Every event value is a list of objects, and every nested `hooks` is a list of objects. Anything else fails the Copilot install outright. The file contributes at least one entry: an empty one deploys nothing, with only a warning. 4. Event names are PascalCase (`PreToolUse`, `PostToolUse`, `UserPromptSubmit`, `SessionStart`, `Stop`, …). An all-lowercase name (`stop`) never warns and never fires; a camelCase name outside apm's rename map (`userPromptSubmit`) deploys verbatim to Claude and never fires. 5. 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. No absolute path and no bare relative path (`scripts/x.sh`): apm bundles and rewrites neither. No `$` or backtick in the path itself. A missing script is only a warning at install time. 6. 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: 7. Every handler sets `"type": "command"` and an explicit `timeout` in seconds — apm passes `type` through but never supplies it. 8. Set `matcher` explicitly on tool events and on `SessionStart` (`startup`, `resume`, …). Omitted, Claude receives `"*"`. 9. Do not author Copilot's flat `bash` / `powershell` / `timeoutSec` keys in a Claude-shaped file; they render onto Claude as stray keys. 10. Quote a script path that may contain spaces: `"${PLUGIN_ROOT}/scripts/my hook.sh"`. 11. Keep helper files in the hook directory non-JSON. Copilot's loader rejects any bundled `.json` without a `hooks` key. 12. Prefer `${PLUGIN_ROOT}` over `${CLAUDE_PLUGIN_ROOT}`. 13. No `hooks-` or `*--hooks` filename. That routing is deprecated and reach belongs to `targets:` (see Gate); the research allows it only when deprecated routing is intended.