agentsizinggate
August 20, 2026 · View on GitHub
What it blocks
An Agent tool call with no recognized model field, or with model set
to a frontier tier (opus/fable) without an explicit acknowledgment that
running it as a leaf subagent is deliberate.
Why this shape is worth a gate
An unset model silently inherits whatever the parent session is running —
often Opus, the most expensive tier — for work that a cheaper model would
have handled identically. The gate's own docstring cites the concrete
motivating measurement: across roughly 1600 spawns in this project's
transcript history, the model-sizing convention was actually consulted only
about a dozen times, and a large share of spawns carried no explicit model
at all. That's not a one-off mistake, it's a default silently winning by
omission on nearly every dispatch. The frontier-leaf half of the rule exists
because a subagent spawned via the Agent tool is always a leaf — it cannot
itself fan out to further subagents — so paying full Opus/Fable rate for
work that can't parallelize is close to strictly worse than either sizing it
down or promoting it to a real standalone orchestrator session that can
fan out.
BLOCKED
$ echo '{"tool_name":"Agent","tool_input":{"prompt":"do the thing","subagent_type":"general-purpose","model":"opus"}}' \
| python3 agent_sizing_gate.py
BLOCKED: Agent(model:"opus") is an Opus leaf — full Opus rate, no fan-out.
no fan-out. ...
$ echo $?
2
ALLOWED
The nearest legitimate thing: the identical spawn, with the narrow-exception sentinel and a real reason in the prompt:
$ echo '{"tool_name":"Agent","tool_input":{"prompt":"opus-leaf-ok: one subtle oppositional review","subagent_type":"general-purpose","model":"opus"}}' \
| python3 agent_sizing_gate.py
$ echo $?
0
Both commands above were run against this tree this session. A separate,
unrelated near-miss: model:"sonnet" or model:"haiku" is never flagged at
all — the frontier tripwire applies only to opus/fable.
The escape marker
opus-leaf-ok: <reason> or fable-leaf-ok: <reason>, anywhere in the spawn
prompt (case-insensitive substring search, not line-anchored the way the
Edit-side markers are — the check scans the whole prompt string). Either
sentinel clears either frontier model; the escape is about "this frontier
leaf is deliberate", not about naming the exact tier. A sentinel with only
whitespace after the colon does not clear the block — _has_leaf_escape
explicitly checks .strip() != "" on the text following it.
Scope
Universal — fires on every Agent tool call, no config file, no env vars.
The hook defensively re-checks tool_name == "Agent" itself even though the
PreToolUse matcher already scopes it to that tool, so a misconfigured
matcher elsewhere cannot make this hook block an unrelated tool call. On
allow(), it emits an agent_spawn telemetry event recording the chosen
model and whether the spawn used isolation: "worktree".
How we know it fires
test_agent_sizing_gate.py, 11 test functions. Run this session, from the
repo root:
$ python3 -m pytest .claude/hooks/test_agent_sizing_gate.py -q
........... [100%]
11 passed in 0.21s
test_fable_leaf_blocked_without_reason asserts rc == 2 and "fable" in err.lower() for an unescaped model="fable" spawn — pinning that Fable
gets the identical frontier treatment as Opus, not a separate weaker check.
test_missing_model_blocked asserts rc == 2 and "does not declare an explicit" in err for a spawn with no model key at all.
test_fable_leaf_blocked_bare_sentinel_no_reason plants
fable-leaf-ok: (whitespace only) and confirms the block still fires —
pinning the reason-required behavior this hook shares with every other
marker in the pack except its Workflow-tool sibling (see that guard's own
page).
Known limits
The hook validates only that model is one of the four recognized string
values (haiku, sonnet, opus, fable) — it has no way to judge whether
the chosen tier is actually appropriate for the work described in the
prompt. A spawn with model:"haiku" on a task that genuinely needs Opus
reasoning passes this hook cleanly; the gate enforces that a choice was
made, not that the choice was right. The escape-marker check is a substring
search over the whole prompt string rather than line-anchored like the
Edit-side markers, so a sentinel embedded anywhere in a long prompt — even
somewhere that reads oddly out of context — still clears the block.
The judgment this gate can't make
This gate checks that a tier was named and that a frontier leaf was deliberate. Deciding which tier the work actually warrants — and whether it should be a leaf at all — is a separate discipline: Agent sizing.