Skip to content

T-H56N-hooks-as-vault-entities

State: planning/proposed · Impact: high · Complexity: large

A hook is the one component every harness documents and Agent Pants cannot author. The renderer library models one and six runtimes can emit one, but no vault-side code path produces a Hook: the string does not appear in agent-adapt-core or in determined-agent-skills at all. This task makes hook a vault kind — a note with typed frontmatter and a body that is the script or the prompt — and carries it through discovery, the build and the tree.

LocationRole today
packages/rust/agent-adapt/src/model/hook.rs#HookCommand-only: name, slug, description, event, matcher, command, timeout_ms, tags, category, is_template
packages/rust/agent-adapt/src/render/hooks_json.rs#render_hooks_jsonEmits a flat { "hooks": [ { event, matcher, command, timeout_ms } ] }, not the harnesses’ Event → [{matcher, hooks:[…]}] nesting
packages/rust/agent-adapt/src/runtime.rs#HookCapabilityrender_hooks, implemented by the Claude Code, Codex, Gemini, OpenCode, Amp and npm runtimes, all delegating to standard::render_hooks
packages/rust/agent-adapt/src/tools.rs#ToolRegistryThe 11 canonical tool names a matcher’s halves translate through, per runtime
solutions/agent-pants/crates/agent-adapt-core/src/frontmatter.rs#EntityKindFive kinds — skill, plugin, command, agent, marketplace. KNOWN lists the accepted spellings
solutions/agent-pants/crates/agent-adapt-core/src/frontmatter.rs#KindSourceRecords which of the four layers answered for a note
solutions/agent-pants/crates/agent-adapt-core/src/discover.rs#KIND_DIRSmarketplaces, plugins, skills, commands, agents — no hooks
solutions/agent-pants/crates/agent-adapt-core/src/export.rs#export_skillsBuilds a marketplace root from skills and commands; nothing in the crate mentions hooks
solutions/determined/crates/determined-core/src/wire/mod.rs#AgentSkillNoteFieldsThe writable frontmatter surface — no hook fields
solutions/determined/experiences/agent-pants/app/composables/useAgentPantsComponentSource.tsThe tree source; mints kind: addresses per kind folder
solutions/determined/experiences/agent-pants/app/utils/fieldSpec.ts#ComponentKind'skill' | 'command' | 'agent' | 'plugin' | 'marketplace'
solutions/agent-pants/docs/vault/decisions/entity-kind-and-frontmatter-namespace.mdAlready names hook as a kind the vocabulary takes later
docs/LIBRARIES.mdThe tier-1 agent-adapt row: the monorepo is canonical and rust-agent-adapt is its public mirror, so growing the model has a mirror consequence

EntityKind::Hook joins the vocabulary and hooks joins KIND_DIRS, so a hook note lives at hooks/<name>.md under whatever holds it, exactly like a command. Its frontmatter models a hook: ap-kind: hook, event, matcher, type, one of command / url / prompt, timeout, if, async, once. The body is the script or the prompt text.

agent-adapt’s Hook grows the fields the canonical superset needs — type, url, prompt, if, async, once — and render_hooks_json emits the nested Event → [{matcher, hooks:[…]}] shape the harnesses actually read. A plugin build writes hooks/hooks.json under the plugin root alongside its skills and commands.

Event names translate per target. Gemini’s BeforeTool and Claude Code’s PreToolUse are the same event and map onto each other; an event with no mapping for that target is refused through HookCapability, naming the event and the target, and the build says so rather than dropping the hook. AgentSkillNoteFields, ComponentKind and the component tree learn the kind so a hook note is discoverable, badged and openable.

A hook note reads:

---
name: block-secret-writes
description: Refuses a write that would land a credential in the repo.
ap-kind: hook
event: PreToolUse
matcher: Edit|Write
type: command
timeout: 5000
---

Growing the tier-1 model is recorded in docs/LIBRARIES.md with its mirror consequence for rust-agent-adapt.

  1. Grow the model in agent-adapt: add type, url, prompt, if, async and once to Hook, each optional so existing constructions still compile. Decision inside this step: type is a closed enum over command, http, mcp_tool, prompt, agent, and an unknown spelling is a parse refusal rather than a pass-through.
  2. Rewrite render_hooks_json onto the nested shape — hooks > Event > [{matcher, hooks:[{type, command|url|prompt, timeout, if, async, once}]}] — keeping matcher translation through ToolRegistry, with fixture tests for the Claude Code and Gemini renders.
  3. Add per-target event translation and refusal to HookCapability, keyed off the event table in T-HLEW-harness-capability-matrix. A target that documents no hook model at all refuses every hook, naming the target.
  4. Add EntityKind::Hook and its KNOWN spelling in agent-adapt-core, and ("hooks", EntityKind::Hook) to KIND_DIRS. KindSource needs no change: the hook kind rides the four layers already there.
  5. Parse the hook frontmatter fields in the ap- reader, and project a note onto agent_adapt::Hook with the body as the command script or the prompt text. Decision inside this step: type: command reads its script from the body, type: prompt reads its prompt from the body, and a note that sets both the body and a command key is refused rather than silently preferring one.
  6. Emit hooks from the build: export_skills collects a plugin’s hook members and writes hooks/hooks.json under that plugin’s root, per target, honouring ap-targets.
  7. Carry the kind up: AgentSkillNoteFields gains the hook fields, ComponentKind gains 'hook', FIELD_SPECS gains one entry per hook field with kinds: ['hook'], and the tree source mints the hooks kind folder.
  8. Add the agent-adapt row note in docs/LIBRARIES.md recording the model growth and the rust-agent-adapt mirror consequence, and document the kind in the vault-layout and frontmatter manual chapters.
AreaNote
packages/rust/agent-adapt/src/model/hook.rstype, url, prompt, if, async, once on Hook; the type enum
packages/rust/agent-adapt/src/render/hooks_json.rsNested Event → [{matcher, hooks:[…]}] output; the new fields
packages/rust/agent-adapt/src/runtime.rsEvent translation and refusal on HookCapability
packages/rust/agent-adapt/src/runtimes/gemini_cli.rsGemini’s event names and the events it does not carry
packages/rust/agent-adapt/src/runtimes/claude_code.rsClaude Code’s event roster as the canonical superset
packages/rust/agent-adapt/tests/Fixture tests for the Claude Code and Gemini hook renders
solutions/agent-pants/crates/agent-adapt-core/src/frontmatter.rsEntityKind::Hook, its KNOWN spelling, the hook frontmatter fields
solutions/agent-pants/crates/agent-adapt-core/src/discover.rshooks in KIND_DIRS
solutions/agent-pants/crates/agent-adapt-core/src/projection.rsproject_hook, the note-to-Hook projection beside project_command
solutions/agent-pants/crates/agent-adapt-core/src/export.rsCollect hook members and write hooks/hooks.json per plugin root
solutions/agent-pants/crates/agent-adapt-core/tests/Fixtures for a hook note, an unmapped event, a both-body-and-command refusal
solutions/determined/crates/determined-core/src/wire/mod.rsHook fields on AgentSkillNoteFields
solutions/determined/crates/determined-agent-skills/src/lib.rsRead and write the hook fields; hook notes in the roster and the tree
solutions/determined/experiences/agent-pants/app/utils/fieldSpec.ts'hook' in ComponentKind; one FIELD_SPECS entry per hook field
solutions/determined/experiences/agent-pants/app/composables/useAgentPantsComponentSource.tsThe hooks kind folder in the tree
docs/LIBRARIES.mdThe model growth and its rust-agent-adapt mirror consequence
solutions/determined/experiences/agent-pants/docs/vault-layout.mdWhere a hook note lives
solutions/determined/experiences/agent-pants/docs/frontmatter-reference.mdThe hook frontmatter fields
  • AC-1: A note at hooks/<name>.md with ap-kind: hook is discovered as a hook, and its kind_source reports which layer answered.
  • AC-2: KIND_DIRS contains hooks, and a hook note in that folder needs no ap-kind key.
  • AC-3: A plugin build writes hooks/hooks.json under the plugin root, in the nested Event → [{matcher, hooks:[…]}] shape, asserted against a fixture.
  • AC-4: A PreToolUse hook renders as BeforeTool for the gemini-cli target, and the matcher’s tool names translate through ToolRegistry in the same render.
  • AC-5: An event with no mapping for a target refuses, naming the event and the target, and the build reports the refusal instead of writing a partial hooks.json.
  • AC-6: A hook note appears in the component tree under a hooks folder and opens in the note editor.
  • AC-7: Hook carries type, url, prompt, if, async and once, and an unknown type spelling is refused.
  • AC-8: The agent-adapt row in docs/LIBRARIES.md records the model growth and the rust-agent-adapt mirror consequence.
  • The hook editor form and the harness gap table: T-0AF7-hook-editor-and-harness-gap-table.
  • MCP servers as a vault kind; hooks are the only kind this task adds.
  • Publishing a standalone hook to a destination outside a plugin — a hook reaches a harness through its plugin’s hooks.json.
  • Updating the rust-agent-adapt mirror. This task records the consequence; the mirror publish is its own release.

Member of milestone M-BMFD, from the user’s planning request of 2026-09-07 on the data model. The namespace decision under solutions/agent-pants/docs/vault/decisions/ — the file entity-kind-and-frontmatter-namespace.md — already reserved hook as a later kind. Event rosters and the nested config shape come from Claude Code hooks, Claude Code plugins and Gemini CLI, all fetched 2026-09-07; the per-harness divergence is catalogued in solutions/agent-pants/docs/vault/reference/harness-comparison.md.

T-HLEW-harness-capability-matrix


← Back to Tasks