@agentguard-run/burn
v0.3.1
Published
Local session usage and runaway-agent circuit breaker. Explain tokens, cache rewrites and API list cost, track pace, warn on heavy turns, and gate agent fan-out with signed receipts. Nothing leaves the machine.
Maintainers
Readme
@agentguard-run/burn
See where a coding session's tokens went and what the next heavy turn could cost in time and limits. Burn explains recorded usage, warns about large cache rewrites and keeps its existing runaway-session circuit breaker. Everything stays on this machine.
Install in five lines
npm i -g @agentguard-run/burn
agentguard-burn init claude
agentguard-burn init codex
agentguard-burn statusline
agentguard-burn whyThe two init commands print hook configuration for review. Merge the relevant snippet into your host configuration. They do not write it. For Codex, review and trust the hook through /hooks. Upgrading an existing hook requires the new snippet: it matches every tool so pace can update on ordinary tool calls, while admission still gates spawns.
For Claude Code, configure its statusLine command as agentguard-burn statusline. It receives the host JSON on stdin. Codex 0.154.0 has no external status command slot. Use agentguard-burn statusline SESSION_ID in a companion terminal and the native Codex limit indicators. See host fields and setup.
Local idle audit
Burn 0.3.0 reports held memory and disk cost exposure on this machine:
agentguard-burn ps
agentguard-burn ps --json
agentguard-burn reapps lists Claude Code and Codex sessions with their Node children, leftover plugin daemons, automation-owned Chrome browsers, and candidate workspaces. Process rows are ranked by resident memory, then workspace rows by disk size. Normal Chrome profiles are excluded. Browser rows show their process count, total resident memory and available window count. Unknown values are explicit. RSS can count shared memory more than once; it is not a bill.
Session idle time uses the controlling terminal's access time when readable, otherwise transcript file-write metadata. The table labels the method. A transcript fallback can warn but cannot prove that a terminal has been inactive for 24 hours. Workspace rows show size, last modification, dirty status and open-handle evidence. No transcript content or process arguments are printed or uploaded. Swap is reported only: swap is only released by the operating system on reboot. Burn does not touch it.
reap prints the same numbered audit, then requires an interactive terminal and numbers typed at the prompt. There is no --yes flag and piped confirmation is refused. It sends only SIGTERM to the selected eligible processes, waits, and reports whether they exited. It never sends SIGKILL and never deletes files or directories.
Reap refuses:
- Any session with terminal input in the last 24 hours, or unavailable terminal-idle evidence. Changing a warning threshold does not change this safety limit.
- Any process whose working directory has modified or untracked files, or whose directory safety cannot be verified.
- Every workspace, including any workspace with open file handles. Workspace rows are information only.
- The process running reap and all its ancestors, including a selected group containing one of them.
- A process whose identity, ownership or safety evidence changed while confirmation was pending. New or replaced PIDs are never added to the confirmed selection.
It rechecks the selected processes before signaling. Permission errors and processes that ignore SIGTERM are reported. They are not force-closed.
Add these fields under thresholds in the existing ~/.agentguard/burn-policy.json (or AGENTGUARD_HOME/burn-policy.json):
{
"thresholds": {
"idle_session_warn_hours": 24,
"idle_browser_warn_minutes": 60,
"orphan_workspace_warn_days": 2
}
}Merge the fields into your existing policy. Missing or invalid values use the shipped defaults. A local teamPolicyFile can point to a JSON file containing thresholds; relative paths resolve from the Burn home. Team threshold values take precedence over local threshold values, including these three audit settings. Neither policy loading nor the audit rewrites your policy.
The Claude Code and Codex hook snippets now include SessionStart. Review the new agentguard-burn init claude or agentguard-burn init codex output to update existing installations. At session start, a fresh five-minute local audit cache is checked. A cache miss gets a bounded scan with a 150 ms budget; timeout or unavailable evidence skips the advisory. A warning is one line, never a table and never a deny:
N idle agent sessions holding X GB, run agentguard-burn psThe line is WARN-only in both shadow and enforce modes. If only browser or workspace rows cross their thresholds, the session count can be zero; run ps for those details. The full ps command refreshes the cache. All collection and caching stays local.
macOS uses local process, file-handle and swap metadata. Linux uses /proc and available local tools, and says what could not be read. Windows prints not supported on this platform yet and exits successfully. See the docs page draft for the audit boundary and limitations.
Why this session
agentguard-burn why
agentguard-burn why SESSION_ID
agentguard-burn rewrites SESSION_ID
agentguard-burn rewrites all
agentguard-burn pace SESSION_ID
agentguard-burn pricingA session argument can also be a transcript path. Without one, Burn uses the session environment variable when available, then the most recently modified local transcript. Claude child transcripts are included in why. Token shares are shares of recorded tokens, not shares of dollars. Repeated usage records for one provider response count once. Copied Codex history is reconciled before counting new usage.
The table covers instruction stack + system, history re-sent, repeated file reads, subagent fan-out, tool output, conversation, full-prefix rewrites, output and unattributed usage. The fixed-prefix baseline is the first assistant response's measured context. It includes the initial user message, so the label is an operational baseline rather than a direct measurement of instruction files. A rewrite classified as prefix change resets that baseline and is counted in the footer; the new baseline can include history already present at that point.
Later cache reads up to the baseline go to instruction stack + system. Reads above it go to history re-sent. Fresh input plus cache creation is the measured arriving increment. Burn subtracts the previous response's output before assigning that increment to intervening tool results and user messages. Result byte sizes only split that measured total; bytes are never converted into tokens. A result for a previously read Read path goes to re-read files. Mixed user and tool intervals are marked shared and counted in the footer.
Rewrite tokens and child transcript usage keep their own buckets. Prior output deducted from an input increment remains in the unattributed residual so every recorded input and output token is counted exactly once. Missing event evidence also remains there. Integer allocation preserves every token, and the Method footer explains each row.
A full-prefix rewrite writes more than 150,000 tokens and more than half the actual input context. Burn reports the observed cache lifetime and surrounding evidence: idle beyond that lifetime, immediate compaction, first subagent response, or residual prefix change. An unknown lifetime stays unknown. Each write is priced at its measured lifetime, or a range when that lifetime is unavailable.
Every tool hook updates a local pace file under the Burn home. Pace uses ten wall-clock minutes: cached means cache reads; uncached means fresh input, writes and output. The next-hour projection assumes that pace continues. A time-to-limit estimate requires rising, fresh host percentage samples from the same quota pool and reset window. No token-to-limit conversion or cache weighting is assumed. Without sufficient host data, the line says why the limit is unknown.
The new warnings are advisory in both modes. Cache rewrites warn once when their last-hour API list equivalent exceeds $5. A context crossing 500,000 tokens warns about processing time and compaction, estimates a cache rewrite, and suggests a fresh session with a handoff note. These warnings do not deny a tool call.
Optional settings in your existing burn-policy.json:
{
"insights": {
"rewriteWarnDollarsPerHour": 5,
"heavyTurnTokens": 500000
}
}Merge these fields into the existing policy. All existing thresholds and modes remain supported. Prices are exact-model API list equivalents, not a subscription bill. Unknown models show tokens only. Every rate has a source and verification date. A local override file and full pricing table are documented in Usage and pricing.
Two safety planes
Structural limits use active time rather than the age of the session. The lifetime spawn count warns at 24 but cannot stop a session on its own. The fan-out ceiling permits 40 spawns within the last 120 active minutes and stops the next proposal. The separate spawn-rate rule warns at 8 and stops candidate 16 within 15 active minutes. Depth above 2 still stops.
Economic limits warn at 3.5B and stop at 5B measured tokens per session. Repeated Claude assistant content blocks share one provider response id. Their usage and copied child history count once, including later increases to a response's recorded usage. Old session counters rebuild once from the parent and its available child transcripts, retaining the signed chain head. The rebuild places events chronologically. A child usage record discovered late on a later hook enters the current active-minute bucket, while session and bucket totals remain conserved. The same numeric sustained thresholds now fire later because inflated usage has been removed. Earlier replay figures in the changelog used the old counts.
Existing policy files keep explicit settings. Missing fields are filled in
memory, with a one-time notice. In particular, an old explicit
spawnRate.enforce: false remains advisory until you change it. New policies
default to true; fanout.windowActiveMinutes defaults to 120. Run calibration
only when you want it to write a new shadow policy, or set AGENTGUARD_HOME
to a scratch directory to review fitted values first.
Price a recorded block
agentguard-burn blocks
agentguard-burn blocks SESSION_ID
agentguard-burn blocks allEach WARN or STOP receipt gets one row with its time, session, detector reasons and spawn ordinal. The row includes the session's available child count, median and maximum tokens, and median and maximum API list-price dollars per child. Forks and fresh agents are reported separately. The report reuses the attribution parser and excludes copied provider responses. It reads recorded usage only and makes no network calls.
These are list-price equivalents, not a bill or a claim of money saved.
Economics describe the currently available child transcripts of that session,
not only the children completed before the receipt. Missing child history or
unknown model pricing stays unavailable. Unknown cache lifetimes produce a
price range. A fork is identified by its transcript's fork metadata. Codex child discovery
requires an explicit parent thread id; unknown inherited-history boundaries
stay unavailable. Use blocks all json for structured output. Session
attribution uses local receipt identifiers without copying transcript content
into the ledger.
Cache-read ratio was about 98% in the original calibration, healthy and pathological alike. It is shown as an explanation and never used to decide.
Start with replay, not with the hook
npx @agentguard-run/burn replayRuns the detectors over your existing history and shows what enforcement would have intercepted, when, and the observed tail after each stop. It is an upper bound, labelled as such. Nobody installs a blocker cold.
npm i -g @agentguard-run/burn
agentguard-burn init --write # merge the hook into ~/.claude/settings.json (backup taken)
agentguard-burn init codex --write # same for ~/.codex/hooks.json
agentguard-burn status # hook health, shadow observations, every live session
agentguard-burn enforce # after 7 days and 50 decisionsThe hook installs in shadow mode: every decision is recorded, nothing is
blocked, until you have seen it be right. status shows what it would have
done so far, which sessions are live on every host, and whether each hook's
command still exists on disk (a hook whose script is gone fails open, and
status says so in capitals).
When it stops you and you disagree
agentguard-burn resume --once --reason "these 60 agents are the plan" # the next STOP passes, once, on any host
agentguard-burn resume --reason "load test" # every STOP passes for 15 minutes
agentguard-burn resume --clearEvery override is written to the decisions ledger with its reason. A
--once override is consumed atomically: two hooks racing for it cannot
both pass. Warnings are spoken once per change in the finding set, not once
per spawn; eighteen identical banners train you to stop reading the
nineteenth.
One policy across hosts (0.2.0)
The detectors never learn which host produced an event. Claude Code, Cursor,
Codex, a local model runtime behind the proxy, and an orchestrator calling
the middleware all normalise into the same event stream, share one
machine-wide reservation lock, and sign the same receipt. The same failure,
through every door, stops at the same step: agentguard-burn conformance
replays a 42-spawn storm and a 250M-token-per-call grind through each adapter
and asserts fan-out WARN at 24, STOP at 41, sustained WARN at 3.5B, STOP at
5B.
What each host can actually see is stated, not implied:
| Host | Spawns | Depth | Usage | How |
|---|---|---|---|---|
| Claude Code | authoritative | authoritative | authoritative | PreToolUse hook + transcript (unchanged from 0.1) |
| Raw middleware | authoritative | authoritative | authoritative | beforeSpawn / beforeCall leases in your orchestrator |
| Ollama proxy | none | none | authoritative | prompt_eval_count + eval_count on the final chunk |
| vLLM / LM Studio / OpenAI-compatible proxy | none | none | authoritative when the server sends usage, else reported missing | non-streaming usage, or the final SSE usage event |
| Cursor (beta) | authoritative | estimated | none | native subagentStart deny; hosted-model usage is never exposed |
| Codex (beta) | authoritative | estimated | estimated | PreToolUse on every tool, with admission on spawn_agent; live deny, allow and override canary passed on codex-cli 0.151.0; transcript parsed best-effort |
An OK from a host that cannot see usage is an OK about spawns, and status
says usage:n/a next to it. Missing usage never becomes a guessed zero.
The full claim, "40 spawns, depth 2, 5B tokens, enforced identically", is true for a deployment that feeds both a topology source and a usage source into one session ID: raw middleware plus the proxy, for instance. A proxy alone sees tokens and no tree. A Cursor hook alone sees the tree and no tokens. The composite conformance check proves the combined case: candidate spawn 41 sees both planes in its findings.
Local models: the compute plane
Token dollars are close to meaningless when the GPU is yours. What runs away
is the machine: concurrency and occupied request time. The proxy tracks both
and warns at 4 concurrent calls by default. No universal STOP ships for
hardware we cannot see; set localCompute.stopConcurrent or
stopOccupiedMs in burn-policy.json for your server. Elapsed request time
includes queueing and transport, so it is called occupied time, never GPU
utilisation.
agentguard-burn proxy --upstream http://127.0.0.1:11434 --host ollama
# point the agent at http://127.0.0.1:18080 and send x-agentguard-session: <id>Loopback only, both sides, by default. Every upstream chunk is written to the client before it is inspected; the observer is a side channel, never a data path. A STOP answers the next request with 429 and the alarm box. It never cuts a stream that is already flowing, and it never kills a running agent. Blocking is not killing.
Raw middleware
import { createRawApiGuard } from '@agentguard-run/burn/middleware';
const burn = createRawApiGuard({ sessionId: 'nightly-refactor-17' });
const spawn = burn.beforeSpawn({ parentDepth: 0 });
spawn.throwIfBlocked();
spawn.started();
try { await worker() } finally { spawn.finished() }
const call = burn.beforeCall({ estimatedTokens: 120_000 });
call.throwIfBlocked();
try {
const res = await client.chat({ ..., headers: call.headers }); // proxy correlates by call id
call.complete({ tokens: res.usage.total_tokens });
} catch (e) { call.fail(); throw e }Usage is committed by call ID and replaces what was reserved under it. When middleware estimated 120K and the proxy later saw 87K for the same call, the session moves by 87K, not 207K.
Cursor and Codex
agentguard-burn init cursor # ~/.cursor/hooks.json snippet, failClosed on
agentguard-burn init codex # ~/.codex/hooks.json snippetBoth renderers are one page each and emit only their host's documented output
object. Codex fails the whole hook on Claude's common fields (continue,
stopReason, suppressOutput), and a failed hook is a fail-open hook, so the
Codex renderer never emits them and a test forbids them by name.
Codex passed a live canary on the installed codex-cli 0.151.0 on
2026-09-03: a spawn_agent call was denied with the alarm box as the reason,
a shell call passed untouched, and a resume --once override let the next
spawn through with the reason on the ledger. Two things the docs got wrong
and the wire settled: the tool arrives as spawn_agent (the docs say it
"matches as Agent"; the matcher covers both), and project-local hooks only
load when the project is trusted. The captured payloads are in
fixtures/codex-0.151.0-pretooluse.json and drive a test.
One step Codex makes you do by hand: after init codex --write, open
codex, run /hooks, and trust the AgentGuard hook. Codex requires this
once per hook source, and there is no CLI for it. Until it is done,
codex exec stalls the first time a spawn would be gated (verified: it
hangs with no output; the same with hooks defined in config.toml).
status repeats this under the codex line because it cannot see whether
trust was granted.
Cursor is verified against the documented schema (permission,
user_message, agent_message; ~/.cursor/hooks.json with failClosed),
not yet against an installed build. It stays beta until a live deny canary
passes.
Receipts
Every spawn decision, and every model call that is not OK, is signed with a local Ed25519 key (Node built-ins, key generated on first use, 0600) and chained to the previous receipt for the session. A receipt carries the host, the coverage, the counts, the verdict, the policy digest and a hash of the session ID. It carries no prompt, completion, path, or tool input. It can leave the machine when a transcript never can.
The concurrency guarantee
Ten parallel Agent calls launch ten hook processes that all read the same
transcript and all see the same count. A naive cap is cosmetic during exactly
the burst it exists for. Spawns are admitted through an atomic, cross-process
reservation under a machine-wide lock; the test suite launches 60 real OS
reservation processes against a cap of 40 and asserts exactly 40 are admitted,
and separately exercises 60 Cursor hook processes. The opt-in stress test runs
240 actual Claude hook processes against cap 40:
npm run build
AGENTGUARD_STRESS=1 node --test dist/tests/stress.test.jsIt checks exactly 40 admitted and 200 denied, verifies the receipt chain for coordinated decisions, and reports lock failures separately. Infrastructure failures deny in enforce mode before a receipt can be chained, as in the gateway.
0.2.0 fixed the lock itself. Under 240 concurrent hook processes the 0.1 lock could tear down a live sibling's lock (a waiter judged "owner is dead" about an instance that had already been released and replaced) and admit 41 to 46. Lock instances now carry a nonce; a reclaim only counts if it grabbed the instance it judged, and every write is fenced on the holder's own nonce still being on the path. In 0.2.3, bounded, staggered retries prevent lock waiters from starving the holder. The Sep 15 audit includes one successful 240-process run after that fix; it does not establish a 192-run guarantee. In 0.2.4, the wait is eight seconds and retirement is serialized before renaming an abandoned lock. The fresh 240-process run admitted exactly 40, denied 200, signed 240 receipts and had zero lock failures. Operational lock-wait failures do not count toward enforce eligibility. See the conservative recovery procedure.
Single-machine by design. Two laptops on one account do not share state, and that is stated rather than hidden.
Account thresholds
account.warnConcurrentSessions warns when more than that many sessions have
been active on this machine in the last 30 minutes, matching status liveness.
Optional account.warnTokens and account.stopTokens sum the last
account.windowActiveMinutes of token buckets from each active session.
Both token limits default to null. Closed gateway sessions are excluded;
hook and gateway session files are both read. WARN never blocks. STOP records
a would-block decision in shadow and blocks new work only in enforce mode.
Terminal canvas and local recordings
Interactive screens use a fixed 104 by 35 cell canvas, with mint for good, slate for neutral, amber for WARN and red for STOP. Smaller terminals drop side detail and paginate rows without wrapping. Use --no-color for the existing plain-text commands and hook cards. Hook JSON and the one-line statusline remain host protocols, with no canvas control sequences inserted.
Any command accepts an explicit local recording path:
agentguard-burn ps --record ./idle-run.jsonl
agentguard-burn status --no-color --record ./status-run.jsonl
agentguard-burn replay --record ./history-run.jsonl
agentguard-burn render ./idle-run.jsonl --mp4 ./idle-run.mp4 --gif ./idle-run.gifEach JSONL frame contains a wall-clock timestamp and structured observations, never rendered terminal text. Audit recordings omit raw process arguments and the full process table. They can contain local paths, PIDs, identifiers and resource measurements, so review them before sharing. Recording is opt-in and stays on disk. A recording failure prints a notice without changing a hook decision or cleanup result. Appended recordings must have nondecreasing timestamps.
The render command draws recorded Canvas cells at 1920 by 1080 using installed Menlo on macOS or DejaVu Sans Mono on Linux. The title says RECORDED RUN and the footer says recorded run. It preserves frame timestamp intervals and holds the final frame for one second. ffmpeg encodes the MP4 and optional GIF; without it, numbered PNG files and a reproducible ffmpeg command remain. No fonts or assets are fetched. The native PNG dependency loads only for render, never during session-start checks.
A provenance JSON beside the requested MP4 records the source recording's SHA256, renderer version, render time, font and frame times. Existing outputs are refused. The source recording stays unchanged. See the render draft for the format and limits.
What it never does
No prompts, responses, file contents, or tool inputs are persisted or rendered. No telemetry. No provider-quota guesses. Account forecasts use only observed host percentages, never an invented allowance or cache weighting.
Local live panel
agentguard-burn live displays spawn and token counts within the active window,
session cache-read share, the policy rate bar and the last eight local decisions.
Use --session ID, --once, --record run.jsonl, or --replay run.jsonl.
STOP reasons hold for two seconds while observations continue. The display is
local and does not affect admission. Missing observations remain unknown.
See the live panel and recorded September 19 evidence.
