[FEATURE] Hook output primitive to require a tool call (`requiredToolCalls`) — hooks can veto and inform, but cannot initiate
Problem
Hooks today have exactly two powers: veto (PreToolUse permissionDecision: deny, Stop/UserPromptSubmit decision: block) and inform (additionalContext, systemMessage). There is no primitive for a hook to initiate a tool call. Any workflow of the shape "when X happens, the model MUST run tool Y" can only be approximated by injecting a directive and then blocking the turn if the model did not comply — i.e. enforcement by regeneration, not by mechanism.
Concrete, measured use case
We built a deterministic model router as a Claude Code plugin (v0.16.0–v0.19.0, all runtime-verified on Claude Code 2.1.172):
- Routing itself is fully deterministic today — this part works and needs nothing: role agents with frontmatter-pinned models (
planner.md→ opus,executor.md→ sonnet) plus a PreToolUse hook that strips any explicitmodelparam viaupdatedInput(probes confirmed:updatedInputreplaces wholesale; an omittedmodelfalls back to the agent frontmatter; an explicit param outranks frontmatter — the hook closes that escape hatch). - Initiation is not deterministic and cannot be: when the user asks for planning in natural language, a UserPromptSubmit hook injects a directive ("spawn
vibe-next:plannerBEFORE answering") and a Stop hook blocks the turn ONCE (loop-guarded viastop_hook_active) if no planner spawn is observed, recording the outcome either way.
Live telemetry of that enforcement loop (check-rate, both outcomes recorded):
- happy path: directive followed,
outcome=complied; - adversarial probe (user prompt explicitly forbade tool use): model obeyed the user, Stop blocked once, model declined again citing the user's instruction,
outcome=ignored_after_block.
The bounded block is the best available approximation, and it costs a full turn regeneration per enforcement, works only once per turn, and remains probabilistic by design.
Proposal
Add an event-appropriate hook output field, e.g. for UserPromptSubmit:
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"requiredToolCalls": [
{
"name": "Agent",
"input": { "subagent_type": "vibe-next:planner", "prompt": "<goal>", "description": "Plan the change" }
}
]
}
}
Semantics (strawman, open to alternatives):
- The harness enqueues the call(s) to run at the start of the assistant turn, before free generation; results enter the turn as ordinary tool_results.
- Tool inputs validate against the tool schema at hook-output time (the same validation
updatedInputalready gets — we measured that failure mode too: a schema-invalid rewrite is rejected loudly withPreToolUse hook for Agent returned updatedInput that failed schema validation, which is exactly the right behavior). - Permission system still applies (a required call that the permission layer denies fails the turn visibly, never silently).
- A weaker variant would already help:
proposedToolCallwith policymust-run-or-must-explain.
Why this matters beyond our plugin
Every "route to a subagent", "always lint after edit", "always read X before Y" automation currently lives in the directive+block pattern. The directive consumes context tokens on every trigger; the block doubles generation on every miss; and user-countermanded cases are unresolvable by construction (correctly so — but a mechanical primitive would let the hook author decide the precedence explicitly instead of leaving it to model judgement).
Environment
- Claude Code 2.1.172 (CLI), Linux
- Probe details and the working router implementation live in a private repo (happy to share specifics on request).