Agent Return Setup — size budget for subagent returns

sonnet status / install / upgrade / enable / disable / uninstall / purge project | global opt-in user-invoked

What 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

SituationSuggested 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

  1. Status first, always

    Before any mode runs, the skill checks both project and global scope for the three hook files, settings.json wiring and an existing config, and prints a state table. Nothing installs or removes blind.

  2. Mode decided from free text

    No arguments defaults to status if installed anywhere, else install. Otherwise the skill reads intent — install, upgrade, enable, disable, uninstall, purge — including RU triggers like “поставь” or “вычисти всё”. Ambiguity between install and a removal verb triggers AskUserQuestion instead of a guess.

  3. Plan stated before any question

    The skill writes out, in plain text, exactly what changes: which files, which paths, which settings.json entries — before asking anything.

  4. Only missing answers are asked

    Scope (Project / Global / Both) and thresholds (default 1000 / 2500, one question yields both numbers) are asked via AskUserQuestion only if not already given. A custom pair must satisfy passTokens < fileTokens or the config write aborts.

  5. File work delegated to hook-creator

    A single brewcode:hook-creator spawn per mode-and-scope combination copies all three files as a unit, merges or strips the two settings.json entries, and writes or edits agent-return.json. Both scopes at once means two spawns in one message.

  6. 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

FileEventBehavior
agent-return-budget.mjsShared 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.mjsSubagentStart (matcher-less)Injects the return contract as additionalContext. Advisory only, no decision
agent-return-guard.mjsSubagentStop (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:

RangeDecisionOrder
t <= passTokens (default 1000)passnone
passTokens < t <= fileTokens (default 2500)blockcompress — re-send the same answer, keep the verdict and every path:line, drop preamble/bodies/output/logs/restated context, no new work
t > fileTokensblockfile — 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 agentstop_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

ModeEffectHook filessettings.jsonConfig
status (default if installed)report only
install (default if not installed)wire + configure3 copied2 entries mergedwritten
upgradere-emit from the current plugin version, thresholds preserved3 re-copiedentries re-mergedvalues preserved, metadata re-stamped
enableresume enforcementkeptkeptenabled: true
disablepause enforcementkeptkeptenabled: false
uninstallunwiredeletedentries strippedkept
purgefull wipedeletedentries strippeddeleted

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

ScopeHooks dirsettings.jsonConfig
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"
}
KeyMeaning
enabledmust be exactly true; anything else, or no config at all, turns both hooks into no-ops — the enable/disable mechanism
passTokenspass ceiling, the number quoted as budget in both block orders; positive integer; default 1000
fileTokenscompress/file tier boundary; positive integer; default 2500
version / generated_by / last_updatedprovenance, 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:

p10p25p50p75p90max
5027611404225631647931

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/4 is not a tokenizer, on purpose. The two thresholds were fitted to a distribution measured with chars/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 < fileTokens is not enforced by the hook itself. Inverting them degrades gracefully — the compress tier vanishes and everything over passTokens gets 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.mjs is 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.

📄

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

Use /brewtools:plugin-update to check and update the brewcode plugin suite in one command. See the FAQ for details.