[FEATURE] Hook output primitive to require a tool call (`requiredToolCalls`) — hooks can veto and inform, but cannot initiate

Open 💬 0 comments Opened Jun 12, 2026 by EfM8caQE9F1v4pFt5x7WrjDI1N2kFqYdydVj

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 explicit model param via updatedInput (probes confirmed: updatedInput replaces wholesale; an omitted model falls 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:planner BEFORE answering") and a Stop hook blocks the turn ONCE (loop-guarded via stop_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 updatedInput already gets — we measured that failure mode too: a schema-invalid rewrite is rejected loudly with PreToolUse 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: proposedToolCall with policy must-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).

View original on GitHub ↗