TEST REPORT: Agent Spend Guard v1.0.0
Run by bash tests/run_all.sh; full output in tests/logs/run-20261030.txt. Started 2026-10-30 09:04:19 GMT, finished 09:04:22 GMT (from date at run time). Box: Linux, Python 3.13.5.
Result: 4/4 steps passed, FAILED STEPS: 0. 42/42 tests: 26 guard (one of them holds 41 nested-agent subtests), 8 installer, 6 usage report, 2 skill.
What is real and what is mocked
- Real:
guard/spend_guard.pyas shipped, run as a separate process fed hook JSON on stdin in Claude Code's and Cursor's documented formats (PreToolUse, PostToolUse, PostToolUseFailure, SubagentStart/Stop, PostToolBatch, UserPromptSubmit; beforeShellExecution, beforeMCPExecution), with its real state files and lock. The installer writing real.claude/settings.jsonand.cursor/hooks.jsonin throwaway projects, then the installed hook command strings run through a shell. - Mocked: the transcripts. Usage numbers come from hand-built JSONL files in the Claude Code transcript layout (
~/.claude/projects/<proj>/<session>.jsonl,subagents/agent-*.jsonl,message.id+message.usage), not from a real Claude Code session. - UNTESTED: the Claude Code and Cursor apps. Neither app is on the test box, so no real agent ran these hooks. Formats come from the official hook docs (code.claude.com/docs/en/hooks; Cursor hooks docs). Before release, install it in one real project for each tool and trigger each rule once.
- UNTESTED: Windows and macOS (Windows path handling is unit-tested only), Python below 3.13, usage-report accuracy against Anthropic's own meter (the weighted-token figure is a relative measure, not the plan meter).
1. Guard: tests/test_guard.py (26/26)
- SPAWN-CONCURRENCY cap and release on SubagentStop; a finished Agent tool call releases its pending slot; pending spawns expire; 6 parallel spawn hooks at once respect the cap (lock test); the older
Tasktool name also counts. SPAWN-TOTAL. - NESTED-AGENT: 41 subtests, positive (e.g.
claude -p,npx -y @anthropic-ai/claude-code -p,cursor-agent,codex exec,$(claude -p …),(cd x && agent "do"),C:\Tools\claude.exe -p x) and negative (claude --version,claude mcp list,grep claude,git commit -m "agent fix"…); levels deny/ask/off; allowlist regex; Cursor shell format. - RETRY-LOOP (3 identical failures; non-zero exit counts; a success resets). REPEAT-CALL with its 30-minute window and exempt tools, in both tools, MCP included.
- SESSION-BUDGET: warns once at 80% ("900 of 1,000"), blocks PostToolBatch and new prompts at 100%,
#spend-okextends by 50%; subagent transcripts counted; a half-written last line isn't counted; missing transcript is fine. - Sessions are kept separate; denials logged; fails open by default and closed when
fail_closed: true;--status. - Latency: median 25 ms, max 27 ms per hook call over 15 runs.
2. Installer: tests/test_installer.py (8/8)
Fresh install of both tools; keeps existing settings and is idempotent; policy edits survive reinstall; --uninstall removes only our hooks; --dry-run writes nothing; invalid JSON stops the install; warns on Cursor CLI approvalMode: unrestricted; installed commands really run from a shell.
3. Usage report: tests/test_report.py (6/6)
Deduplication by message.id and totals; per-session rows; --days filter; SPIKE / FAN-OUT / LOOP / UNATTENDED flags; budget suggestion, text and CSV output; missing transcript folder.
4. Skill: tests/test_skill.py (2/2)
Frontmatter and references valid; ASCII file names; no key-shaped strings.
Failures seen during development (fixed before the logged run)
- NESTED-AGENT missed
npx -y @anthropic-ai/claude-code -pand(cd x && agent "do"). Fixed by mappingclaude-codeto claude and splitting segments on parentheses. - NESTED-AGENT missed a Windows path (
C:\Tools\claude.exe) on Linux. Fixed by splitting on both slash types. - One budget test expected no warning at 900/1,000; that was wrong (90% is past the 80% warning), so the test was corrected, not the code.
--uninstallleft an empty{"version":1,"hooks":{}}in.cursor/hooks.json; now removed.
Nothing was published, deployed or sent.
Real-app verification 2026-10-30
Re-run by bash tests/run_all.sh. Full output is in tests/logs/run-20261030-realapp.txt; it started at 12:00:17 GMT and finished at 12:00:24 GMT.
Result: 5/5 steps passed, FAILED STEPS: 0. 49/49 tests:
- 27 guard tests. The nested-agent test now has 57 subtests.
- 8 installer, 6 usage report, 2 skill.
- 6 inside the real Claude Code CLI (new file tests/test_real_claude.py).
Installed
- Claude Code 2.1.314:
npm i -g @anthropic-ai/claude-code. - Cursor CLI
cursor-agent2026.10.22-27dcf5f: officialcurl https://cursor.com/install -fsS | bash.
Claude Code: real runs, PASS
- How it ran:
claude -pran withANTHROPIC_BASE_URLpointing at a local scripted Messages API (tests/real_app/mock-anthropic.mjs) and a dummy key. - The CLI, its settings loader, its hook runner and its tools (Bash, Agent/Task, Workflow) are the real ones. Only the model's replies are scripted.
- No Anthropic account was used and nothing was spent.
- Settings load: with the guard installed,
claude doctorshows no "Invalid settings" section. A deliberately broken hook event name in a scratch project does produce "Invalid settings" with the list of valid events. - The guard's event names (
PreToolUse,PostToolUse,PostToolUseFailure,SubagentStart,SubagentStop,UserPromptSubmit,Stop) are all in that list. - Real hook payloads were captured by a logging hook.
- PreToolUse carries
tool_use_idandtool_input. - Bash PostToolUse has
tool_response {stdout, stderr, interrupted…}and noexit_code. A failed command arrives asPostToolUseFailurewitherror: "Exit code 2…". The guard's retry-loop rule reads that event. - SubagentStart and SubagentStop carry
agent_idandagent_type; SubagentStop also carriesagent_transcript_path. - The Agent tool launches subagents asynchronously, and denials name the tool
Task. - Nested agent: a Bash call running
claude -p …was denied. The model receivedPreToolUse:Bash hook error: Agent Spend Guard [NESTED-AGENT]: …. - Retry loop: denied after 3 real failures of the same command.
- Session budget: 5,010 weighted tokens against a 1,000 budget, then
terminal_reason: hook_stopped. - Spawn cap: with the real Agent tool, exactly 2 subagents spawned and the third was denied.
- Workflow fan-out: denied. Under
-pan "ask" becomes a deny, because nobody is there to answer. usage_report.pyread the real transcripts under~/.claude/projects/…, includingsubagents/agent-*.jsonl.
Bugs found and fixed
- The safe-subcommand lists didn't match the real CLIs.
- Checked against
claude --help2.1.314 andcursor-agent --help2026.10.22. agent lswas treated as safe, but it resumes a session; it is now guarded.- Added as safe:
- claude:
auth,logs,stop,kill,rm,upgrade,plugins,auto-mode,import. - cursor-agent:
status,whoami,login,logout,models,--list-models,about,team,create-chat,mcp,plugin,bedrock, shell-integration commands.
- claude:
- Kept as spawning (guarded):
claude --bg,agents,attach,respawn,ultrareview;cursor-agent persist,worker,resume,generate-rule. - New subtests cover each of these.
- New fan-out path not covered. Claude Code 2.1.314 has a
Workflowtool that runs multi-agent workflows. - New rule WORKFLOW-FANOUT: policy
fanout_tools: ["Workflow"]andfanout_tool_level: "ask"(it can be set to deny or off). - Tested in the unit suite and in the real CLI.
- Docs claimed Cursor has no subagent hooks. It does (
subagentStart/subagentStop). The README, LISTING and SKILL now say this version uses Cursor's shell and MCP hooks only.
Cursor: static check PASS, live run BLOCKED
- Checked against the installed cursor-agent bundle and cursor.com/docs/agent/hooks and /permissions:
- Locations
.cursor/hooks.jsonand~/.cursor/hooks.json. - Schema
{version: number, hooks: {…}}. beforeShellExecution/beforeMCPExecutionresponses{permission: allow|deny|ask, user_message, agent_message}.timeoutin seconds.beforeMCPExecutiontool_inputis a JSON string.- The guard's install and output match all of these.
- Also confirmed: Cursor loads Claude-style hooks from
.claude/settings*.json("third-party hooks"). They need the project to be trusted, and the docs now say so. - BLOCKED: a live Cursor agent run with the hooks.
cursor-agentsaid "Your stored authentication is invalid… run 'agent login'", andcursor-agent statusreports "Not logged in". This needs a Cursor account login (agent login, orCURSOR_API_KEY). It does not need a paid plan to install, but running the agent uses the account's model quota.
Re-run 2026-11-02 (promoted from drafts/2026-10-30 to products/)
bash tests/run_all.sh from products/agent-spend-guard/; full output in tests/logs/run-20261102.txt (file written 2026-11-02 07:13:54 GMT).
Result: FAILED STEPS: 0, 49/49 tests: 27 guard, 8 installer, 6 usage report, 2 skill, 6 real Claude Code CLI hook runs (model replaced by a local mock; no Anthropic account used).
Unchanged limits: no live Cursor run (blocked earlier, see above); no paid model calls.
Pre-launch re-run 2026-11-02
bash tests/run_all.sh; full output in tests/logs/run-20261102-prelaunch.txt (2026-11-02, about 08:00 GMT). FAILED STEPS: 0, 49/49 tests (27 guard, 8 installer, 6 usage report, 2 skill, 6 real Claude Code CLI hook runs with the model replaced by a local mock). Counts unchanged. Still no live Cursor run.