# Hooks Reference for this plugin's hook definitions: where to edit them, where they end up, and what the host reads. This document lives in `docs/` rather than next to the hooks it describes. `plugins/kyberforge/hooks/` is a **generated mirror** — `scripts/sync-plugin-content.sh` runs `rm -rf` on it before every rebuild, so any hand-written file placed there is deleted on the next sync with no drift warning (a prior copy of this document was lost exactly that way). See ADR-0017. ## Where to edit Author hooks in `plugins/kyberforge/.apm/hooks/*.json`. `apm pack --format plugin` merges every file in that directory into a single `hooks.json`, which `sync-plugin-content.sh` copies to `plugins/kyberforge/hooks/hooks.json` — the path Claude Code convention-scans. Never edit the mirrored file; the `check-plugin-content-sync` pre-push hook reports it as drift. ## Claude Code structure `hooks/hooks.json` is read by Claude Code. Structure: ```json { "hooks": { "PostToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "echo 'tool used'" } ] } ] } } ``` Events (**partial list**): `PreToolUse`, `PostToolUse`, `Notification`, `Stop`. Claude Code's plugin hook set is larger — `SessionStart`, `SessionEnd`, `UserPromptSubmit`, `PreCompact` and `SubagentStop` also exist — and this repo's vendored corpus does not enumerate it anywhere: `docs/research/docs/claude-code-plugins/configuration.md:100` describes the file as "Event handlers (PreToolUse, PostToolUse, etc.)", and `agent-definition.md:53` covers only the per-agent `hooks` field, not the plugin-level set. Treat the four names above as the ones this repo has verified, not as the schema. Check Claude Code's own hooks documentation before wiring an event not listed here. Use `${CLAUDE_PLUGIN_ROOT}` to reference scripts inside this plugin — the plugin runs from a cache path after install, not its original repo location. ## GitHub Copilot CLI Copilot reads a differently-shaped `hooks.json`: `version: 1` is required, each entry is `type: "command"` with separate `bash` and `powershell` scripts, and the lifecycle points are lowercase and differently named (`sessionStart`, `sessionEnd`, `userPromptSubmitted`, `preToolUse`, `postToolUse`, `errorOccurred`, `agentStop`). See `docs/research/docs/github-copilot-plugins/configuration.md`. There is no separate Copilot hooks file at this plugin root, and — as things stand — **Copilot resolves to no hooks file at all.** Two corrections to an earlier revision of this document, which got both halves of this wrong: **The deleted root `hooks.json` was not a stale sync artifact.** `plugins/kyberforge/hooks.json` was added in `2287ddc` (2026-06-20), the commit that created the plugin, well before `scripts/sync-plugin-content.sh` existed; `plugins/lint/hooks.json` arrived the same way in `f326df4`. Main's Copilot manifest `plugins/kyberforge/plugin.json` declared `"hooks": "hooks.json"`, and `plugins/lint/plugin.json` did the same — these were deliberately pointed-at Copilot hooks files, not leftovers. The sync (`38f1ba4`) later took ownership of that path, and ADR-0017's 2026-08-14 amendment moved the generated file to `hooks/hooks.json` because that, not the plugin root, is the path Claude Code convention-scans. **Only Claude Code resolves to `hooks/hooks.json`.** Claude Code finds it by auto-discovery. Copilot does not: `docs/research/docs/github-copilot-plugins/configuration.md:47` types `hooks` as a `plugin.json` field of type "string or object" with **no default**, so there is no convention path to scan, and `jq 'has("hooks")'` returns `false` for all six `.github/plugin/plugin.json` files that `apm pack` emits. With the pointer gone and no auto-discovery to fall back on, the Copilot ecosystem sees zero hooks. The effect looks like the twin of the `mcpServers` gap that ADR-0017's 2026-08-13 amendment re-injects for: same "string or object" type, same absence of a default, same outcome of a Copilot manifest with no pointer. The *mechanism* differs, and ADR-0017 is explicit about it — `mcpServers` is actively stripped by `build_plugin_manifest`, whereas `hooks` "was never in `build_plugin_manifest`'s strip list at all"; it is simply never emitted, because `apm.yml` has no key that produces one. So this is an absence apm never fills, not a removal to reverse. ## Why no `hooks` pointer is injected **Decided (2026-08-14, PR #95): the gap stays documented rather than patched.** `sync-plugin-content.sh` does *not* re-inject a `hooks` pointer into `.github/plugin/plugin.json`, and a test pins that absence. Full reasoning is in ADR-0017's "no `hooks` pointer" amendment; the short version, because the one-line fix looks obvious and someone will propose it again: The `mcpServers` re-injection is safe because `.mcp.json` is **one format both ecosystems read**, so the pointer is a true statement about the file whatever it contains. Hooks have no shared format. Compare the two structures above: Claude Code wants `PreToolUse` with `matcher` objects; Copilot requires `version: 1`, lowercase event names, and per-shell `bash`/`powershell` keys. And apm merges `.apm/hooks/*.json` into **exactly one** `hooks.json` with no per-target shaping — the same file Claude Code convention-scans. One file, two incompatible readers. So a pointer would tell Copilot that a Claude-shaped file is Copilot-shaped: an incomplete manifest traded for a wrong one. It is not inert even today — `{"hooks": {}}` has no `version: 1`, so the pointer would name a file invalid against the very schema it is pointed at from. And it does not become correct later: whoever writes the first real hook writes it in one shape, and it is the Claude shape in practice, since Claude Code auto-discovers the same file and is what hooks here are authored against. **What this costs you:** a hook authored under `.apm/hooks/` reaches Claude Code and not Copilot. That is a real limitation, and it is the accepted one until apm emits a per-target hooks file or the two schemas converge. If you need a Copilot hook today, raise it — it needs an upstream change or a second authoring path, not a pointer. ## Symlinks under `.apm/` do not survive Do not author any file under `plugins/kyberforge/.apm/` as a symlink. apm's bundle exporter filters symlinks out of the bundle entirely and says nothing, so the file never reaches the mirror. Since `sync-plugin-content.sh` builds both sides of its drift comparison from that same bundle, the loss used to be invisible to `--check` as well. `check_apm_symlinks()` now reads the `.apm/` source tree directly and fails the sync with the offending path — replace the symlink with a regular file. It stays quiet about one place: `.apm///tests/`, the dev-fixture directory the mirror excludes anyway (a symlink there loses nothing, because nothing under it is mirrored). A `tests/` deeper than that — `assets/templates/tests/`, a scaffolding asset the mirror does carry — is reported like anywhere else. See ADR-0017's symlink amendment.