Agent Return Setup — size budget for subagent returns
sonnet status / install / upgrade / enable / disable / uninstall / purge project | global opt-in user-invokedWhat it does
Subagent returns are the largest single context cost in a manager session. The rule “verdict first, at most 30 lines, path:line” already existed as prose and was ignored — a rule sitting at the top of context loses to whatever the agent just did. /brewtools:agent-return-setup restates that rule mechanically, at the exact moment it bites.
It installs two hooks sharing one module: a SubagentStart contract hook injects the return rule as advisory context, and a SubagentStop guard hook sizes the final message against two thresholds (mechanics below). No LLM judge anywhere — the decision is a plain number comparison.
Run the skill with no arguments and it reports status first — installed scopes, wired hooks, current thresholds — before doing anything else. Every mode, including status, prints the 5-field PLAN block up front; the mutating ones (install, upgrade, enable, disable, uninstall, purge) then ask only what is still unknown before delegating the file and settings work to the brewcode:hook-creator agent. The feature is opt-in — these hooks are not registered in brewtools/hooks/hooks.json, so installing the plugin does nothing until this skill runs.
When to use
| Situation | Suggested invocation |
|---|---|
| Manager sessions losing context to bloated subagent reports | /brewtools:agent-return-setup install |
| Same budget on every project on this machine | /brewtools:agent-return-setup install global |
| Pick up a new hook version after a brewtools update, same thresholds | /brewtools:agent-return-setup upgrade |
| Check what is currently installed without changing anything | /brewtools:agent-return-setup status |
| Temporarily stop enforcing without removing files | /brewtools:agent-return-setup disable |
| Fully remove hooks, settings entries and config | /brewtools:agent-return-setup purge |
Example
/brewtools:agent-return-setup install project 1000 2500
Expected flow: the skill prints the current status table, states the plan (“copy 3 files into .claude/hooks/, write .claude/agent-return.json with passTokens: 1000, fileTokens: 2500, merge 2 settings.json entries”), asks nothing — scope and both thresholds are already supplied, so the question step is skipped entirely, delegates to brewcode:hook-creator, then re-prints the status table showing hook_files=3/3 settings_refs=2 enabled=true.
Workflow
- Status first, always
Before any mode runs, the skill checks both project and global scope for the three hook files,
settings.jsonwiring and an existing config, and prints a state table. Nothing installs or removes blind. - Mode decided from free text
Empty input defaults to status whether installed or absent. Explicit modes win; otherwise whole-word intent selects a mode. Destructive ambiguity is clarified before actions.
- Plan stated before any question
The skill writes out, in plain text, exactly what changes: which files, which paths, which
settings.jsonentries — before asking anything. - Only missing answers are asked
Scope (Project / Global / Both) and thresholds (default 1000 / 2500, one question yields both numbers) are asked via
AskUserQuestiononly if not already given. A custom pair must satisfypassTokens < fileTokensor the config write aborts. - File work delegated to hook-creator
A single
brewcode:hook-creatorspawn per mode-and-scope combination copies all three files as a unit, merges or strips the twosettings.jsonentries, and writes or editsagent-return.json. Both scopes at once means two spawns in one message. - Refreshed status printed
The status block re-runs. Hook wiring changes (install/upgrade/uninstall/purge) need a new session; config value changes (
enabled,passTokens,fileTokens) are read live, no restart.
Technical details
The three files
| File | Event | Behavior |
|---|---|---|
agent-return-budget.mjs | — | Shared module: config discovery, threshold resolution, estimateTokens, the contract text. Never registered in settings.json — imported by both hooks, so the announced budget and the enforced budget can never drift apart |
agent-return-contract.mjs | SubagentStart (matcher-less) | Injects the return contract as additionalContext. Advisory only, no decision |
agent-return-guard.mjs | SubagentStop (matcher-less) | Sizes last_assistant_message, blocks at most once with a compress or a file order |
All three install as a unit. ESM resolution runs before evaluation, so a hooks dir holding 2 of the 3 files exits 1 with an empty stdout and a hook-error banner on every subagent spawn and return — never copy 2 of 3.
Sizing tiers, t = Math.ceil(last_assistant_message.length / 4), both boundaries inclusive on the low side:
| Range | Decision | Order |
|---|---|---|
t <= passTokens (default 1000) | pass | none |
passTokens < t <= fileTokens (default 2500) | block | compress — re-send the same answer, keep the verdict and every path:line, drop preamble/bodies/output/logs/restated context, no new work |
t > fileTokens | block | file — write the detail to .claude/reports/YYYYMMDD-HHMMSS_<agent-slug>/, then answer with that path plus verdict plus at most 3 lines |
Blocks at most once per agent — stop_hook_active === true is checked before any sizing, so a compress round that lands slightly over passTokens (observed live: 1417 -> 1026 est-tokens against a budget of 1000) is not blocked a second time. Deliberate trade: a SubagentStop hook that blocks twice is how an agent gets wedged, and the once-only guarantee outranks the last few percent.
Modes
| Mode | Effect | Hook files | settings.json | Config |
|---|---|---|---|---|
status (default with empty input) | report only | — | — | — |
install | wire + configure | 3 copied | 2 entries merged | written |
upgrade | re-emit from the current plugin version, thresholds preserved | 3 re-copied | entries re-merged | values preserved, metadata re-stamped |
enable | resume enforcement | kept | kept | enabled: true |
disable | pause enforcement | kept | kept | enabled: false |
uninstall | unwire | deleted | entries stripped | kept |
purge | full wipe | deleted | entries stripped | deleted |
upgrade asks nothing: it reads passTokens/fileTokens back out of the existing config and replays the install for that scope against the current assets, so a plugin update finally reaches an installed project without re-asking. A disabled setup stays disabled.
Installation targets
| Scope | Hooks dir | settings.json | Config |
|---|---|---|---|
| Project | <repo>/.claude/hooks/ | <repo>/.claude/settings.json | <repo>/.claude/agent-return.json |
| Global | ~/.claude/hooks/ | ~/.claude/settings.json | ~/.claude/agent-return.json |
Config discovery walks up from the hook process’s cwd (16 levels), probing <dir>/.claude/agent-return.json, then falls back to ~/.claude/agent-return.json. Project wins; a malformed project config is skipped and global takes over. Global writes go through Bash only — ~/.claude/* is a protected path, blocked for Write/Edit in every permission mode.
Config shape
{
"enabled": true,
"passTokens": 1000,
"fileTokens": 2500,
"version": "X.Y.Z",
"content_version": "X.Y.Z",
"generated_by": "brewtools:agent-return-setup",
"last_updated": "YYYY-MM-DD"
}
| Key | Meaning |
|---|---|
enabled | must be exactly true; anything else, or no config at all, turns both hooks into no-ops — the enable/disable mechanism |
passTokens | pass ceiling, the number quoted as budget in both block orders; positive integer; default 1000 |
fileTokens | compress/file tier boundary; positive integer; default 2500 |
version / content_version / generated_by / last_updated | Provenance restamped on install, upgrade, enable, and disable; content_version tracks changed generator content, while version identifies the producing plugin release |
Threshold precedence, per threshold, first hit wins: the config key — the env var (AGENT_RETURN_PASS / AGENT_RETURN_FILE, parsed with Number() not parseInt()) — the built-in 1000 / 2500. Only a positive integer is accepted at either level; 1.7, abc, -5, 0, NaN, Infinity all fall through.
Config values are read on every hook call — changing enabled or either threshold takes effect immediately, no restart. Hook wiring changes need a new session.
Setup-status compares each artifact’s content_version with the source for that artifact, then corroborates copied hook bytes and ownership. Hook and config-generator content versions can differ, and unchanged content need not match a newer plugin release number. A missing comparison source is reported as unknown, not a fabricated stale verdict.
Evidence behind 1000 / 2500
Measured over 80 real Agent returns across 4 session transcripts, sized chars/4:
| p10 | p25 | p50 | p75 | p90 | max |
|---|---|---|---|---|---|
| 502 | 761 | 1404 | 2256 | 3164 | 7931 |
Total 136.7k est-tokens, of which 79.4k (58%) is overflow above 800 — preamble, restated context, pasted file bodies, command output, logs. 1000 is the grace line: p25 (761) already sits under it, so a genuinely terse return is never touched, and it cuts at the median. 2500 is roughly p78 — past it, compression cannot reach 1000 without losing content.
Live proof, one real session, two blocked returns each blocked exactly once: 1417 -> 1026 est-tokens (compress tier) and 2585 -> 245 est-tokens citing a report path (file tier) — 2731 est-tokens of manager context saved across two returns.
Warning
Honest limits — read before relying on this.
chars/4is not a tokenizer, on purpose. The two thresholds were fitted to a distribution measured withchars/4. Swapping the heuristic moves the boundaries off the data — re-measure and re-fit both in the same change.- Only the final assistant message is sized. A subagent that burned context on 40 tool calls and returns 6 lines is invisible to this guard — it budgets the return, not the work.
passTokens < fileTokensis not enforced by the hook itself. Inverting them degrades gracefully — the compress tier vanishes and everything overpassTokensgets a self-contradictory file order; no loop, no error, exit 0. The config write step rejects the inversion; the hook carries no validator.- Fail-open everywhere, never exit 2 except to block. Malformed JSON, missing stdin, wrong shapes, any runtime throw ->
{}and exit 0. - A missing
agent-return-budget.mjsis not catchable — ESM resolution precedes evaluation, so the hook exits 1 with empty stdout, a non-blocking hook-error banner. Ship all three files or none. - Cost: two hooks per subagent, not a per-tool-call tax. Measured on Node v24.1.0, 15 invocations each, wall clock including node startup: guard p50 33 ms / max 56 ms; contract p50 31 ms / max 33 ms; a 200000-char message still p50 32 ms — node startup dominates, message size barely registers. Registered timeout is 5 s. Unlike
agent-deadline-setup, whose guard sits on a.*PreToolUse matcher and taxes roughly 58 ms on every tool call, this pair fires only at subagent spawn and stop, so a global install is cheap. Numbers are from one machine — re-measure before quoting them as facts.
Coexistence with agent-deadline-setup
Both skills install side by side. Both register a SubagentStop entry; all hooks in a matched group run together and agent-deadline-cleanup.mjs always returns {}, so there is no competing decision. Each skill’s merge and strip only touch entries naming its own scripts.
Verification
155 test cases / 803 checks, all passing — tests/run.sh in the skill source; run it from a checkout of the repo.
Related
Brewtools overview
All brewtools skills and agents in one place.
Agent Deadline Setup
A soft wall-clock budget for subagents — installable side by side with agent-return, both share the SubagentStop event.
Setup Status
Read-only dashboard across every -setup skill — installed, stale or missing, with the hand-run command for each.
GitHub source
SKILL.md, the install runbook, and all three hook files.
Updating plugins
/brewtools:plugin-update to check and update the brewcode plugin suite in one command.
See the FAQ for details.