primitive-author: - description excludes read-only review (-> factory-audit) - validation Gotcha now matches the research: compile never reads prompts, install fails only on a bad Copilot hook payload and warns on prompt input names and dropped keys - instruction fold-in into AGENTS.md/CLAUDE.md stated as conditional on dedup and --force-instructions - hook checklist gains the wrapped-shape Must, drops hardlinks, notes why executable is stricter than the research, and states the separate Copilot-targeted package route instead of a blanket "don't" - prompt Must 5 keeps the research's Copilot-only-key exception; adds model-slug and 250-char Shoulds; descriptions name skills or agents - placeholder instruction covers both FILL IN and FILL_IN_ tokens factory-audit: hardlink FAIL scoped to instructions and prompts (find_hook_files skips symlinks only), with bats cases; prompt-flow description rubric names skills or agents. forge: version-bump, apm-routes and sources references updated for the primitive route. Refs #94 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
4.0 KiB
4.0 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}also works but ties the source to one harness's name.- 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. - 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. - 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. - The script is referenced as
${PLUGIN_ROOT}/…(package root) or./…(hook directory), exists inside the package, and is executable. No absolute path, and no$or backtick in the path itself. A missing script is only a warning at install time. Executable is stricter than the research's Should: a script invoked as the command's first token fails at runtime without it. - No
hooks-<target>or*-<target>-hooksfilename. That routing is deprecated; reach belongs totargets:(see Gate).
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. - Quote a script path that may contain spaces:
"${PLUGIN_ROOT}/scripts/my hook.sh". - Keep helper files in the hook directory non-JSON. Copilot's loader rejects any bundled
.jsonwithout ahookskey.