Files
holocron/plugins/kyberforge/docs/hooks.md
Defame1297 f4bb1cf4e5 docs: finish reconciling the agent write fence across the toolchain
The previous round taught agent-audit's validator to permit disallowedTools but
left the skill that writes agents still forbidding it, in six places. Running
agent-author on any of the three fenced orchestrators would have stripped the
fence, and nothing would have caught it: the validator's allowlist is a permit
list, so an absent field passes. The template was the worst of them, since its
comment is copied verbatim into every new plugin-scope agent.

Where a list had to be restated it is now a pointer to field-inventory.md's
apm-agent-allowlist instead -- the same data validate.sh reads -- because a
roster copied into a template goes stale one step further out than the roster
itself. Where the text has to teach something it teaches the shape rule rather
than the exception: tools is an allowlist whose vocabulary differs per harness,
so verbatim copy makes one value wrong on one target; disallowedTools is a
denylist, where an unrecognised name denies nothing, so the worst case is a
missing fence rather than a wrongly granted capability.

ADR-0016's amendment claimed an unrecognised key is inert on Copilot while the
same ADR's Context says that behaviour is unconfirmed by research -- asserting
as settled the exact thing it flags as unknown, and justifying it with apm's
compile-time behaviour, which says nothing about Copilot's runtime. It is
rewritten into labelled tiers: confirmed for Claude Code with citations,
inferred by analogy for Copilot with the analogy's limits stated, unverified
where it is unverified, and the residual risk accepted explicitly with its
blast radius. It also no longer claims to restore a write sandbox: the denylist
does not deny Bash, which these agents inherit and legitimately need.

docs/hooks.md called the old root hooks.json a stale sync artifact -- it was
added in the plugin's creating commit and pointed at by main's Copilot manifest
-- and claimed both ecosystems now resolve hooks/hooks.json. Copilot does not:
its hooks field has no default and no compiled manifest declares one, so it
resolves nothing. Recorded as the gap it is, with re-injection noted as a
follow-up rather than asserted away. Its event list is marked partial.

Also: new-agent.bats asserted a hardcoded four-field allowlist and would have
rejected a scaffolded agent carrying the field the ADR now blesses; it reads
field-inventory.md too. And ADR-0016's premise that Claude's tools: is
space-separated was wrong -- it takes a comma-separated string or a YAML list.
The incompatibility with Copilot is the vocabulary, not the punctuation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT
2026-08-14 12:30:30 +00:00

5.0 KiB

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:

{
  "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 is 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. It is harmless today only because both generated files are empty ({"hooks": {}}), so nothing is being lost. The first real hook authored under .apm/hooks/ would ship to Claude Code and silently not to Copilot.

Follow-up, not fixed here: decide whether sync-plugin-content.sh should re-inject a hooks pointer into .github/plugin/plugin.json the way reinject_mcp_servers() already does for mcpServers. That decision has a prerequisite — the two hook schemas differ (see above), so a single hooks/hooks.json pointed at from both manifests would hand Copilot a file shaped for Claude Code. Reconcile the schemas before wiring the pointer.