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
No arguments defaults to
statusif installed anywhere, elseinstall. Otherwise the skill reads intent —install,upgrade,enable,disable,uninstall,purge— including RU triggers like “поставь” or “вычисти всё”. Ambiguity between install and a removal verb triggersAskUserQuestioninstead of a guess. - 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 if installed) | report only | — | — | — |
install (default if not installed) | 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",
"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 / generated_by / last_updated | provenance, re-stamped on every write; status compares version against the installed plugin to flag a stale install |
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.
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.