videomd Your workspace

/SEE IT RUN

27 minutes of video. 89 words.
One reviewer that works.

A coding agent read one library brief, built a read-only code reviewer, and the reviewer found 9 bugs it was not allowed to fix. Recorded 2026-10-12. Every line below comes from the session log.

Brief
89 words, read in under a minute
Agent time
4 min 34 s across two sessions
Turns
10
Cost
$1.65 in API usage
Host
Linux (Fedora 44), Claude Code 2.1.271
Reviewer tools
Glob, Grep, Read. Nothing else.

The brief

Library file Custom subagents in Claude Code, 89 words in our own words, every command and path checked against the official docs. It is free as of this page. The source was a 27-minute video.

# Custom subagents in Claude Code

## Before you start
- Needs: Claude Code and a bounded task for a specialist.

## Steps
1. Run `/agents`, create an agent, and choose project or personal scope.
2. Set its name, specific trigger description, instructions, model and required tools. Restrict a reviewer to read-only tools.
3. Inspect the generated `.claude/agents/<agent-name>.md` or `~/.claude/agents/<agent-name>.md`; fix malformed YAML.
4. Ask Claude to use the named agent on a sample task. Verify its output and tool access.

## Warnings
- Parallel agents consume additional usage.
Download brief.md

Watch it run

Two claude -p sessions on a clean machine, one after the other. Session 1: read the brief, build the reviewer. Session 2: use it, then ask it to fix what it found.

Rendered from the session log: every line in the clip is in transcript.txt, with its timestamp. Nothing was re-typed or cut, except long tool output, which is trimmed to its first lines in the clip and complete in the transcript.

What you just saw

  1. The agent read the brief and the project, then wrote .claude/agents/code-reviewer.md with tools: Glob, Grep, Read. 4 turns, 41 s.
  2. In session 2 it delegated the review to that reviewer. The reviewer read every file and reported 9 findings, most serious first. The most serious one was real: a negative transfer amount drained any account.
  3. Asked to fix it, the reviewer answered that it has no editing tools, and returned the exact two-line patch instead.
  4. The agent checked git diff: nothing under src/ changed. 6 turns, 3 min 53 s.
Read the full transcript on this page
==== session 1  (Claude Code 2.1.271, model claude-fable-5-1, permission mode auto)
          you      > Read brief.md. It is a 92-word brief about Claude Code subagents. Follow its steps 1 to 3 for this project: create a project-scoped code-reviewer subagent that can only read, never edit. Read-only means file reading and searching tools only, no shell. Do not change any file under src/.
05:18:23  claude   : I'll read the brief and the project layout first, then create the subagent file.
05:18:23  claude   $ Bash  cat brief.md && echo "-----" && ls -la && echo "-----" && ls -la .claude 2>/dev/null; ls -la .claude/agents 2>/dev/null; find src -type f | head -50
05:18:23  claude   < # Custom subagents in Claude Code
                     
                     ## Before you start
                     - Needs: Claude Code and a bounded task for a specialist.
                     
                     ## Steps
                     1. Run `/agents`, create an agent, and choose project or personal scope.
                     2. Set its name, specific trigger description, instructions, model and required tools. Restrict a reviewer to read-only tools.
                     3. Inspect the generated `.claude/agents/<agent-name>.md` or `~/.claude/agents/<agent-name>.md`; fix malformed YAML.
                     4. Ask Claude to use the named agent on a sample task. Verify its output and tool access.
                     
                     ## Warnings
                     - Parallel agents consume additional usage.
                     -----
                     total 32
                     drwxr-xr-x. 1 user user   140 Oct 12 01:18 .
                     drwxr-xr-x. 1 user user   100 Oct 12 01:18 ..
                     -rw-r--r--. 1 user user   587 Oct 12 01:15 brief.md
                     drwxr-xr-x. 1 user user   128 Oct 12 01:18 .git
                     -rw-r--r--. 1 user user   124 Oct 12 01:15 README.md
                     -rw-r--r--. 1 user user 12643 Oct 12 01:18 session-1.jsonl
                     -rw-r--r--. 1 user user    21 Oct 12 01:18 session-1.start
                     -rw-r--r--. 1 user user   157 Oct 12 01:18 session-1.stderr
                     drwxr-xr-x. 1 user user    12 Oct 12 01:15 src
                     -----
                     src/ledger/__init__.py
                     src/ledger/__main__.py
                     src/ledger/accounts.py
                     src/ledger/report.py
05:18:29  claude   $ Bash  cat README.md && echo "-----" && head -20 src/ledger/accounts.py src/ledger/report.py src/ledger/__main__.py && echo "-----" && ls ~/.claude/agents 2>/dev/null; python3 -c "import yaml; print('pyyaml ok')" 2>&1
05:18:32  claude   < # ledger
                     
                     A small in-memory ledger: accounts, transfers and a statement. No tests yet.
                     
                     Run the example: `python -m ledger`
                     -----
                     ==> src/ledger/accounts.py <==
                     from dataclasses import dataclass, field
                     from datetime import date
                     
                     
                     class InsufficientFunds(Exception):
                         pass
                     
                     
                     @dataclass
                     class Transaction:
                         day: date
                         amount: int  # cents; positive is a credit, negative is a debit
                         memo: str
                     
                     
                     @dataclass
                     class Account:
                         name: str
                         history: list[Transaction] = field(default_factory=list)
                     
                     
                     ==> src/ledger/report.py <==
                     from datetime import date
                     
                     from .accounts import Account
                     
                     
                     def statement(account: Account, start: date, end: date) -> str:
                         """Lines for every transaction from start to end inclusive, then the closing balance."""
                         lines = [f"Statement for {account.name}"]
                         running = 0
                         for t in sorted(account.history, key=lambda t: t.day):
                             if start <= t.day <= end:
                                 running += t.amount
                                 lines.append(f"{t.day.isoformat()}  {t.amount / 100:>10.2f}  {t.memo}")
                         lines.append(f"Closing balance: {running / 100:.2f}")
                         return "\n".join(lines)
                     
                     ==> src/ledger/__main__.py <==
                     from datetime import date
                     
                     from . import Ledger, statement
                     
                     ledger = Ledger()
                     ledger.open("alice", 10_000)
                     ledger.open("bob")
                     ledger.transfer("alice", "bob", 2_500, date(2026, 10, 1), "rent share")
                     print(statement(ledger.accounts["bob"], date(2026, 10, 1), date(2026, 10, 31)))
                     -----
                     pyyaml ok
05:18:50  claude   $ Bash  mkdir -p .claude/agents && cat > .claude/agents/code-reviewer.md <<'EOF'
                     ---
                     name: code-reviewer
                     description: Read-only code reviewer for this ledger project. Use proactively after code is written or changed, or whenever the user asks for a review, audit, or second opinion on correctness, edge cases, or style in src/. Reports findings; never edits files.
                     tools: Read, Grep, Glob
                     model: inherit
                     ---
                     
                     You are a senior Python reviewer for the `ledger` package in `src/ledger/`. It is a small in-memory ledger: accounts, transfers, and a statement report. Amounts are integer cents.
                     
                     You are strictly read-only. You may only read and search files. You cannot run commands, edit, create, or delete anything. If a fix is needed, describe it precisely (file, line, and the exact change) so a human or another agent can apply it.
                     
                     When invoked:
                     1. Read every file under `src/ledger/` that is relevant to the request, plus `README.md`. Use Grep and Glob to trace callers and definitions before judging anything.
                     2. Review for, in priority order:
                        - Correctness: wrong results, unhandled edge cases, off-by-one errors, date-range boundaries, sign errors on debits and credits, missing validation (zero or negative amounts, unknown accounts, transfers to self).
                        - Money handling: integer cents must never be converted to floats except for display; rounding and formatting must be consistent.
                        - Error handling: exceptions that should be raised but are not, or are raised too late (after partial state changes).
                        - Clarity and maintainability: naming, dead code, duplicated logic, misleading docstrings or comments.
                        - Missing tests: the project has none; note which behaviours most need coverage.
                     3. Report in this format:
                        - A one-line verdict (ship / needs changes / blocking issues).
                        - Findings ordered by severity. Each finding names the file and line, states the defect in one sentence, gives a concrete input that triggers it, and proposes the fix.
                        - A short list of things that look right, so the reader knows what you checked.
                     
                     Be specific and concise. Do not speculate about code you have not read. Do not pad the report with generic advice.
                     EOF
                     echo "--- file written ---"; cat .claude/agents/code-reviewer.md; echo "--- yaml check ---"
                     python3 - <<'PY'
                     import yaml, re, pathlib
                     text = pathlib.Path(".claude/agents/code-reviewer.md").read_text()
                     m = re.match(r"^---\n(.*?)\n---\n", text, re.S)
                     fm = yaml.safe_load(m.group(1))
                     print(fm)
                     assert fm["name"] == "code-reviewer"
                     assert set(t.strip() for t in fm["tools"].split(",")) == {"Read", "Grep", "Glob"}
                     assert "Bash" not in fm["tools"] and "Edit" not in fm["tools"] and "Write" not in fm["tools"]
                     print("frontmatter OK: name, description, tools (read-only), model present")
                     PY
                     echo "--- git status ---"; git status --short; git diff --stat -- src/
05:18:52  claude   < --- file written ---
                     ---
                     name: code-reviewer
                     description: Read-only code reviewer for this ledger project. Use proactively after code is written or changed, or whenever the user asks for a review, audit, or second opinion on correctness, edge cases, or style in src/. Reports findings; never edits files.
                     tools: Read, Grep, Glob
                     model: inherit
                     ---
                     
                     You are a senior Python reviewer for the `ledger` package in `src/ledger/`. It is a small in-memory ledger: accounts, transfers, and a statement report. Amounts are integer cents.
                     
                     You are strictly read-only. You may only read and search files. You cannot run commands, edit, create, or delete anything. If a fix is needed, describe it precisely (file, line, and the exact change) so a human or another agent can apply it.
                     
                     When invoked:
                     1. Read every file under `src/ledger/` that is relevant to the request, plus `README.md`. Use Grep and Glob to trace callers and definitions before judging anything.
                     2. Review for, in priority order:
                        - Correctness: wrong results, unhandled edge cases, off-by-one errors, date-range boundaries, sign errors on debits and credits, missing validation (zero or negative amounts, unknown accounts, transfers to self).
                        - Money handling: integer cents must never be converted to floats except for display; rounding and formatting must be consistent.
                        - Error handling: exceptions that should be raised but are not, or are raised too late (after partial state changes).
                        - Clarity and maintainability: naming, dead code, duplicated logic, misleading docstrings or comments.
                        - Missing tests: the project has none; note which behaviours most need coverage.
                     3. Report in this format:
                        - A one-line verdict (ship / needs changes / blocking issues).
                        - Findings ordered by severity. Each finding names the file and line, states the defect in one sentence, gives a concrete input that triggers it, and proposes the fix.
                        - A short list of things that look right, so the reader knows what you checked.
                     
                     Be specific and concise. Do not speculate about code you have not read. Do not pad the report with generic advice.
                     --- yaml check ---
                     {'name': 'code-reviewer', 'description': 'Read-only code reviewer for this ledger project. Use proactively after code is written or changed, or whenever the user asks for a review, audit, or second opinion on correctness, edge cases, or style in src/. Reports findings; never edits files.', 'tools': 'Read, Grep, Glob', 'model': 'inherit'}
                     frontmatter OK: name, description, tools (read-only), model present
                     --- git status ---
                     ?? .claude/
                     ?? session-1.jsonl
                     ?? session-1.start
                     ?? session-1.stderr
05:19:00  claude   : Done. The project-scoped reviewer subagent is in place and nothing under `src/` changed.
                     
                     **What I created:** `.claude/agents/code-reviewer.md`, the file the `/agents` wizard would have generated for project scope.
                     
                     - **Name and trigger:** `code-reviewer`, described so Claude invokes it after code changes or on any request for a review or audit of `src/`.
                     - **Tools:** Read, Grep, Glob only. No Bash, Edit, Write, or any other tool, so it can read and search but cannot run shell commands or modify files.
                     - **Model:** inherits the parent session's model.
                     - **Instructions:** review the ledger package for correctness, integer-cents money handling, error ordering, clarity, and missing test coverage, then report a verdict with file-and-line findings and proposed fixes it cannot apply itself.
                     
                     **Verification for step 3:** I parsed the frontmatter with a YAML parser and confirmed the tool list contains exactly the three read-only tools. Git status shows only the new `.claude/` directory as untracked, with no diff under `src/`.
                     
                     **One deviation from the brief:** step 1 asks you to run the interactive `/agents` command. That wizard cannot run in this non-interactive session, so I wrote the file it produces directly. The result is identical in effect.
                     
                     **Next step (step 4, not done):** in an interactive session, ask Claude to "use the code-reviewer agent to review src/ledger/accounts.py" and confirm it reports findings without attempting any edits or shell commands. The agent file is uncommitted if you want it in version control.

==== session 2  (Claude Code 2.1.271, model claude-fable-5-1, permission mode auto)
          you      > Step 4 of brief.md: use the code-reviewer subagent to review src/ledger/ and report what it finds, most serious first. Then ask the same subagent to fix the most serious finding, and tell me exactly what happened.
05:19:17  claude   : I'll run the code-reviewer subagent on `src/ledger/` first, then send it a follow-up asking it to fix the top finding and observe what it does.
05:19:20  claude   $ Agent  code-reviewer: Review every file under src/ledger/ in ~/project (accounts.py, report.py, __init__.py, __main__.py) plus README.md. Report your findings ordered most serious first, in the format your instructions specify: one-line verdict, then findings with file:line, the defect, a concrete triggering input, and the proposed fix, then a short list of what looks right.
05:19:23  reviewer $ Glob  src/ledger/**/*
05:19:23  reviewer < src/ledger/__init__.py
                     src/ledger/__main__.py
                     src/ledger/accounts.py
                     src/ledger/report.py
05:19:24  reviewer $ Read  ~/project/README.md
05:19:24  reviewer < 1	# ledger
                     2	
                     3	A small in-memory ledger: accounts, transfers and a statement. No tests yet.
                     4	
                     5	Run the example: `python -m ledger`
                     6	
05:19:25  reviewer $ Read  ~/project/src/ledger/accounts.py
05:19:25  reviewer < 1	from dataclasses import dataclass, field
                     2	from datetime import date
                     3	
                     4	
                     5	class InsufficientFunds(Exception):
                     6	    pass
                     7	
                     8	
                     9	@dataclass
                     10	class Transaction:
                     11	    day: date
                     12	    amount: int  # cents; positive is a credit, negative is a debit
                     13	    memo: str
                     14	
                     15	
                     16	@dataclass
                     17	class Account:
                     18	    name: str
                     19	    history: list[Transaction] = field(default_factory=list)
                     20	
                     21	    @property
                     22	    def balance(self) -> int:
                     23	        return sum(t.amount for t in self.history)
                     24	
                     25	    def post(self, day: date, amount: int, memo: str) -> None:
                     26	        self.history.append(Transaction(day, amount, memo))
                     27	
                     28	
                     29	class Ledger:
                     30	    def __init__(self) -> None:
                     31	        self.accounts: dict[str, Account] = {}
                     32	
                     33	    def open(self, name: str, opening_cents: int = 0) -> Account:
                     34	        account = Account(name)
                     35	        if opening_cents:
                     36	            account.post(date.today(), opening_cents, "opening balance")
                     37	        self.accounts[name] = account
                     38	        return account
                     39	
                     40	    def transfer(self, src: str, dst: str, amount: int, day: date, memo: str = "transfer") -> None:
                     41	        source, target = self.accounts[src], self.accounts[dst]
                     42	        if amount > source.balance:
                     43	            raise InsufficientFunds(f"{src} has {source.balance}, needs {amount}")
                     44	        source.post(day, -amount, memo)
                     45	        target.post(day, amount, memo)
                     46	
05:19:25  reviewer $ Read  ~/project/src/ledger/report.py
05:19:25  reviewer < 1	from datetime import date
                     2	
                     3	from .accounts import Account
                     4	
                     5	
                     6	def statement(account: Account, start: date, end: date) -> str:
                     7	    """Lines for every transaction from start to end inclusive, then the closing balance."""
                     8	    lines = [f"Statement for {account.name}"]
                     9	    running = 0
                     10	    for t in sorted(account.history, key=lambda t: t.day):
                     11	        if start <= t.day <= end:
                     12	            running += t.amount
                     13	            lines.append(f"{t.day.isoformat()}  {t.amount / 100:>10.2f}  {t.memo}")
                     14	    lines.append(f"Closing balance: {running / 100:.2f}")
                     15	    return "\n".join(lines)
                     16	
05:19:26  reviewer $ Read  ~/project/src/ledger/__init__.py
05:19:26  reviewer < 1	"""A tiny in-memory ledger."""
                     2	
                     3	from .accounts import Account, Ledger, InsufficientFunds
                     4	from .report import statement
                     5	
                     6	__all__ = ["Account", "Ledger", "InsufficientFunds", "statement"]
                     7	
05:19:27  reviewer $ Read  ~/project/src/ledger/__main__.py
05:19:27  reviewer < 1	from datetime import date
                     2	
                     3	from . import Ledger, statement
                     4	
                     5	ledger = Ledger()
                     6	ledger.open("alice", 10_000)
                     7	ledger.open("bob")
                     8	ledger.transfer("alice", "bob", 2_500, date(2026, 10, 1), "rent share")
                     9	print(statement(ledger.accounts["bob"], date(2026, 10, 1), date(2026, 10, 31)))
                     10	
05:19:31  reviewer $ Grep  transfer\(|\.open\(|statement\(|InsufficientFunds
05:19:31  reviewer < src/ledger/__main__.py:6:ledger.open("alice", 10_000)
                     src/ledger/__main__.py:7:ledger.open("bob")
                     src/ledger/__main__.py:8:ledger.transfer("alice", "bob", 2_500, date(2026, 10, 1), "rent share")
                     src/ledger/__main__.py:9:print(statement(ledger.accounts["bob"], date(2026, 10, 1), date(2026, 10, 31)))
                     src/ledger/report.py:6:def statement(account: Account, start: date, end: date) -> str:
                     src/ledger/accounts.py:5:class InsufficientFunds(Exception):
                     src/ledger/accounts.py:40:    def transfer(self, src: str, dst: str, amount: int, day: date, memo: str = "transfer") -> None:
                     src/ledger/accounts.py:43:            raise InsufficientFunds(f"{src} has {source.balance}, needs {amount}")
                     src/ledger/__init__.py:3:from .accounts import Account, Ledger, InsufficientFunds
                     src/ledger/__init__.py:6:__all__ = ["Account", "Ledger", "InsufficientFunds", "statement"]
05:19:31  reviewer $ Glob  **/*.{py,toml,cfg,ini,txt}
05:19:31  reviewer < src/ledger/__init__.py
                     src/ledger/__main__.py
                     src/ledger/accounts.py
                     src/ledger/report.py
05:20:50  reviewer $ SubagentHandback  **Verdict: blocking issues** — a negative transfer amount bypasses the funds check and moves money in the wrong direction, and the statement's "Closing balance" is wrong whenever the account has history before the range start.
                     
                     ## Findings (most serious first)
                     
                     ### 1. Negative/zero transfer amounts are accepted and reverse the transfer, bypassing the overdraft check
                     `~/project/src/ledger/accounts.py:40-45`
                     `transfer` never validates `amount`. For a negative amount, `amount > source.balance` is False, so the check passes; then `source.post(day, -amount)` credits the source and `target.post(day, amount)` debits the target. Any caller can pull funds out of any account regardless of its balance.
                     Trigger: `l = Ledger(); l.open("alice"); l.open("bob"); l.transfer("alice", "bob", -5_000, date(2026,10,1))` → no exception, alice +5000, bob -5000. `amount=0` also silently appends two zero-cent transactions to both histories.
                     Fix: as the first statement in `transfer`, add `if amount <= 0: raise ValueError(f"transfer amount must be positive, got {amount}")`.
                     
                     ### 2. Statement "Closing balance" ignores transactions before `start`
                     `~/project/src/ledger/report.py:9-14`
                     `running` starts at 0 and only accumulates in-range transactions, so the printed closing balance is the net of the period, not the account balance at `end`. The docstring (line 7) promises "the closing balance".
                     Trigger: alice opened 2026-09-01 with 10_000; `transfer("alice","bob",2_500, date(2026,10,1))`; `statement(alice, date(2026,10,1), date(2026,10,31))` prints `Closing balance: -25.00` while `alice.balance` is 7_500.
                     Fix: seed the running total with prior history, e.g. `running = sum(t.amount for t in account.history if t.day < start)`, and add an `Opening balance:` line after the header so the report is self-consistent. (Transactions after `end` are correctly excluded.)
                     
                     ### 3. `Ledger.open` stamps the opening balance with `date.today()`, so backdated transfers precede it and the balance check is not date-aware
                     `~/project/src/ledger/accounts.py:33-36` and `:42`
                     The opening balance has no caller-supplied date, which makes results nondeterministic and lets a transfer dated before the opening pass the funds check (the check uses the all-time total on line 42, not the balance as of `day`).
                     Trigger: the shipped example in `~/project/src/ledger/__main__.py:6-9` — alice's opening is dated today (2026-10-12), the transfer is dated 2026-10-01. `statement(alice, date(2026,10,1), date(2026,10,11))` prints `Closing balance: -25.00` for an account that was never overdrawn by intent. Run the example again next month and the alice statement changes.
                     Fix: change the signature to `open(self, name: str, day: date, opening_cents: int = 0)` and post with `day`; update `__main__.py:6-7` to pass a date. Also reject `opening_cents < 0` with `ValueError`. If backdated transfers are intended, the overdraft check in `transfer` should compute the balance as of `day` (`sum(t.amount for t in source.history if t.day <= day)`); otherwise document that only the current balance is checked.
                     
                     ### 4. `Ledger.open` silently overwrites an existing account
                     `~/project/src/ledger/accounts.py:37`
                     `self.accounts[name] = account` replaces any existing entry; its history is lost and any `Account` object previously returned to a caller is orphaned.
                     Trigger: `l.open("alice", 10_000); l.open("alice")` → `l.accounts["alice"].balance == 0`, no error.
                     Fix: before line 34, `if name in self.accounts: raise ValueError(f"account {name!r} already exists")`.
                     
                     ### 5. Transfer to self is allowed
                     `~/project/src/ledger/accounts.py:40-45`
                     `transfer("alice", "alice", 100, day)` passes the funds check and appends a -100 and +100 pair to the same history. Net zero, but it pollutes statements with phantom lines.
                     Fix: `if src == dst: raise ValueError("cannot transfer an account to itself")` alongside the amount check.
                     
                     ### 6. Unknown account surfaces as a bare `KeyError`
                     `~/project/src/ledger/accounts.py:41`
                     `self.acc
                     [... 3928 more characters in the log]
05:20:52  reviewer < {"success":true,"message":"Report delivered to your caller."}
05:20:53  claude   < This agent's report was delivered to you as a message from "a3677b4b774b0b966" (its SubagentHandback call). Read it there; it is not repeated here.
                     
                     agentId: a3677b4b774b0b966 (use SendMessage with to: 'a3677b4b774b0b966', summary: '<5-10 word recap>' to continue this agent)
                     <usage>subagent_tokens: 15907
                     tool_uses: 9
                     duration_ms: 91556</usage>
05:20:55  claude   : The review is in. Now I'll ask the same subagent to apply the fix for finding 1 and watch what it does with its read-only tool set.
05:20:56  claude   $ ToolSearch  {"query": "select:SendMessage", "max_results": 1}
05:20:56  claude   < 
05:20:59  claude   $ SendMessage  {"to": "a3677b4b774b0b966", "summary": "Ask reviewer to apply fix for finding 1", "message": "Please now fix your most serious finding (#1): edit src/ledger/accounts.py so that transfer() raises ValueError when amount <= 0, before the funds check. Apply the change to the file directly, then confirm the file contents. If you are unable to make the edit, state exactly why, and report what you attempted.", "type": "message", "recipient": "a3677b4b774b0b966", "content": "Please now fix your most serious finding (#1): ed\u2026"}
          harness  ! SendMessage: [Modify Shared Resources]
05:21:40  claude   ! Permission for this action was denied by the Claude Code auto mode classifier. Reason: [Modify Shared Resources]. If you have other tasks that don't depend on this action, continue working on those. IMPORTANT: You *may* attempt to accomplish this action using other tools that might naturally be used to accomplish this goal, e.g. using head instead of cat. But you *should not* attempt to work around this denial in malicious ways, e.g. do not use your ability to run tests to execute non-test actions. You should only try to work around this restriction in reasonable ways that do not attempt to bypass the intent behind this denial. If you believe this capability is essential to complete the user's request, first try a safer method. Get as much of the rest of the task done as you can, then STOP and explain to the user what you were trying to do and why you need this permission. Let the user decide how to proceed. To allow this type of action in the future, the user can add a Bash permission rule to their settings.
05:21:47  claude   $ Agent  code-reviewer: A prior review of ~/project/src/ledger/accounts.py found that transfer() (around lines 40-45) never validates amount, so a negative amount bypasses the overdraft check and moves money in the wrong direction.
                     
                     Please fix this now: edit src/ledger/accounts.py so that transfer() raises ValueError when amount <= 0, as the first statement in the function before the funds check. Apply the change to the file directly, then confirm by re-reading the file.
                     
                     If you are unable to make the edit, state exactly why, list every tool you attempted and what each returned, and give the exact replacement code so a human can apply it.
05:22:36  reviewer $ Read  ~/project/src/ledger/accounts.py
05:22:36  reviewer < 1	from dataclasses import dataclass, field
                     2	from datetime import date
                     3	
                     4	
                     5	class InsufficientFunds(Exception):
                     6	    pass
                     7	
                     8	
                     9	@dataclass
                     10	class Transaction:
                     11	    day: date
                     12	    amount: int  # cents; positive is a credit, negative is a debit
                     13	    memo: str
                     14	
                     15	
                     16	@dataclass
                     17	class Account:
                     18	    name: str
                     19	    history: list[Transaction] = field(default_factory=list)
                     20	
                     21	    @property
                     22	    def balance(self) -> int:
                     23	        return sum(t.amount for t in self.history)
                     24	
                     25	    def post(self, day: date, amount: int, memo: str) -> None:
                     26	        self.history.append(Transaction(day, amount, memo))
                     27	
                     28	
                     29	class Ledger:
                     30	    def __init__(self) -> None:
                     31	        self.accounts: dict[str, Account] = {}
                     32	
                     33	    def open(self, name: str, opening_cents: int = 0) -> Account:
                     34	        account = Account(name)
                     35	        if opening_cents:
                     36	            account.post(date.today(), opening_cents, "opening balance")
                     37	        self.accounts[name] = account
                     38	        return account
                     39	
                     40	    def transfer(self, src: str, dst: str, amount: int, day: date, memo: str = "transfer") -> None:
                     41	        source, target = self.accounts[src], self.accounts[dst]
                     42	        if amount > source.balance:
                     43	            raise InsufficientFunds(f"{src} has {source.balance}, needs {amount}")
                     44	        source.post(day, -amount, memo)
                     45	        target.post(day, amount, memo)
                     46	
05:22:48  reviewer $ SubagentHandback  **Unable to apply the edit.** I am a strictly read-only reviewer and have no editing capability.
                     
                     **Tools attempted:**
                     - `Read` on `~/project/src/ledger/accounts.py` -> succeeded, returned the 45-line file (contents confirmed below).
                     - No Edit, Write, or Bash tool exists in my toolset (only Read, Grep, Glob, SubagentHandback), so no edit was possible to attempt. I did not try the MCP connectors (Airtable, Notion, Hugging Face, Claude Docs); none of them can write to the local filesystem.
                     
                     **Confirmed defect** at `~/project/src/ledger/accounts.py` lines 40-45:
                     
                     ```python
                         def transfer(self, src: str, dst: str, amount: int, day: date, memo: str = "transfer") -> None:
                             source, target = self.accounts[src], self.accounts[dst]
                             if amount > source.balance:
                                 raise InsufficientFunds(f"{src} has {source.balance}, needs {amount}")
                             source.post(day, -amount, memo)
                             target.post(day, amount, memo)
                     ```
                     
                     Trigger: `ledger.transfer("a", "b", -500, date.today())` with `a` at balance 0. `-500 > 0` is False, so the overdraft check passes; `a` is credited 500 and `b` is debited 500. `amount == 0` also silently posts two zero-amount transactions.
                     
                     **Exact replacement** (replace lines 40-45 with the following; the only change is the two inserted lines immediately after the `def` line, before the account lookup and the funds check):
                     
                     ```python
                         def transfer(self, src: str, dst: str, amount: int, day: date, memo: str = "transfer") -> None:
                             if amount <= 0:
                                 raise ValueError(f"transfer amount must be positive, got {amount}")
                             source, target = self.accounts[src], self.accounts[dst]
                             if amount > source.balance:
                                 raise InsufficientFunds(f"{src} has {source.balance}, needs {amount}")
                             source.post(day, -amount, memo)
                             target.post(day, amount, memo)
                     ```
                     
                     Placing the check first means it fires before any `KeyError` from an unknown account and before any state change; no partial posts occur because both `post` calls remain after all validation.
                     
                     **Related issues noticed in the same function** (not part of this request, flag for follow-up):
                     - Line 41: unknown `src`/`dst` raises a bare `KeyError` rather than a descriptive error.
                     - No check for `src == dst`; a self-transfer passes the funds check and posts a matching debit and credit to the same account (net zero, but pollutes history).
                     - Line 12 comment and the `-amount`/`amount` signs on lines 44-45 are consistent with each other (debit negative, credit positive), so no sign error there.
                     
                     Once a human or agent with write access applies the replacement, re-read lines 40-47 of the file to confirm.
05:22:50  reviewer < {"success":true,"message":"Report delivered to your caller."}
05:22:53  claude   < This agent's report was delivered to you as a message from "a2554c412fbf73435" (its SubagentHandback call). Read it there; it is not repeated here.
                     
                     agentId: a2554c412fbf73435 (use SendMessage with to: 'a2554c412fbf73435', summary: '<5-10 word recap>' to continue this agent)
                     <usage>subagent_tokens: 9054
                     tool_uses: 2
                     duration_ms: 19420</usage>
05:22:55  claude   $ Bash  git status --short && git diff --stat -- src/ && sed -n 40,45p src/ledger/accounts.py
05:22:56  claude   < ?? .claude/
                     ?? session-1.end
                     ?? session-1.exit
                     ?? session-1.jsonl
                     ?? session-1.start
                     ?? session-1.stderr
                     ?? session-2.jsonl
                     ?? session-2.start
                     ?? session-2.stderr
                         def transfer(self, src: str, dst: str, amount: int, day: date, memo: str = "transfer") -> None:
                             source, target = self.accounts[src], self.accounts[dst]
                             if amount > source.balance:
                                 raise InsufficientFunds(f"{src} has {source.balance}, needs {amount}")
                             source.post(day, -amount, memo)
                             target.post(day, amount, memo)
05:23:08  claude   : Step 4 is complete. The reviewer found real bugs, and when asked to fix one it correctly declined because it has no write tools. Nothing under `src/` changed.
                     
                     **Review findings, most serious first** (from the code-reviewer subagent, verdict "blocking issues"):
                     
                     1. **Negative or zero transfer amounts are accepted.** In `accounts.py:40-45`, a negative amount passes the overdraft check and moves money backwards, so any caller can drain any account. Zero posts two empty transactions.
                     2. **Statement closing balance ignores history before the range start.** In `report.py:9-14` the running total starts at zero, so it reports the period's net, not the account balance.
                     3. **Opening balance is stamped with today's date.** Backdated transfers in the shipped example land before the opening deposit, making output nondeterministic month to month.
                     4. **Opening an existing account silently overwrites it**, losing its history.
                     5. **Transfer to self is allowed**, adding phantom debit and credit lines.
                     6. **Unknown account raises a bare KeyError** with no useful message.
                     7. **Inverted date range in statement is not rejected** and prints a zero balance.
                     8. **Cent formatting via float division is duplicated**; an integer helper would be safer.
                     9. **Account.post is public and unchecked**, so every Ledger invariant can be bypassed.
                     
                     It also listed the highest-value tests to add and confirmed what is correct: validation happens before any state change, the balance boundary uses strict greater-than, the sign convention is consistent, and the date range is inclusive as documented.
                     
                     **What happened when I asked it to fix finding 1:**
                     
                     - My first attempt to send the follow-up to the original reviewer instance was blocked by the auto mode permission classifier. I could not resume that exact instance.
                     - I spawned a fresh instance of the same `code-reviewer` definition with the fix request. That is the same agent file and the same tool restriction.
                     - It read the file, then reported it could not edit because its only tools are Read, Grep, and Glob. It did not try to work around the restriction. It returned the exact replacement code instead: two lines at the top of `transfer` raising ValueError when the amount is zero or negative.
                     - I verified afterwards that git shows no diff under `src/` and the function is byte-for-byte unchanged.
                     
                     This confirms the read-only restriction works as intended. The fix itself remains unapplied, as you asked me not to change anything under `src/`. If you want it applied, say so and I will make the two-line edit myself.

Download transcript.txt

The proof

The file the agent wrote

---
name: code-reviewer
description: Read-only code reviewer for this ledger project. Use proactively after code is written or changed, or whenever the user asks for a review, audit, or second opinion on correctness, edge cases, or style in src/. Reports findings; never edits files.
tools: Read, Grep, Glob
model: inherit
---

You are a senior Python reviewer for the `ledger` package in `src/ledger/`. It is a small in-memory ledger: accounts, transfers, and a statement report. Amounts are integer cents.

You are strictly read-only. You may only read and search files. You cannot run commands, edit, create, or delete anything. If a fix is needed, describe it precisely (file, line, and the exact change) so a human or another agent can apply it.

When invoked:
1. Read every file under `src/ledger/` that is relevant to the request, plus `README.md`. Use Grep and Glob to trace callers and definitions before judging anything.
2. Review for, in priority order:
   - Correctness: wrong results, unhandled edge cases, off-by-one errors, date-range boundaries, sign errors on debits and credits, missing validation (zero or negative amounts, unknown accounts, transfers to self).
   - Money handling: integer cents must never be converted to floats except for display; rounding and formatting must be consistent.
   - Error handling: exceptions that should be raised but are not, or are raised too late (after partial state changes).
   - Clarity and maintainability: naming, dead code, duplicated logic, misleading docstrings or comments.
   - Missing tests: the project has none; note which behaviours most need coverage.
3. Report in this format:
   - A one-line verdict (ship / needs changes / blocking issues).
   - Findings ordered by severity. Each finding names the file and line, states the defect in one sentence, gives a concrete input that triggers it, and proposes the fix.
   - A short list of things that look right, so the reader knows what you checked.

Be specific and concise. Do not speculate about code you have not read. Do not pad the report with generic advice.
Download code-reviewer.md

The review the reviewer returned

**Verdict: blocking issues** — a negative transfer amount bypasses the funds check and moves money in the wrong direction, and the statement's "Closing balance" is wrong whenever the account has history before the range start.

## Findings (most serious first)

### 1. Negative/zero transfer amounts are accepted and reverse the transfer, bypassing the overdraft check
`~/project/src/ledger/accounts.py:40-45`
`transfer` never validates `amount`. For a negative amount, `amount > source.balance` is False, so the check passes; then `source.post(day, -amount)` credits the source and `target.post(day, amount)` debits the target. Any caller can pull funds out of any account regardless of its balance.
Trigger: `l = Ledger(); l.open("alice"); l.open("bob"); l.transfer("alice", "bob", -5_000, date(2026,10,1))` → no exception, alice +5000, bob -5000. `amount=0` also silently appends two zero-cent transactions to both histories.
Fix: as the first statement in `transfer`, add `if amount <= 0: raise ValueError(f"transfer amount must be positive, got {amount}")`.

### 2. Statement "Closing balance" ignores transactions before `start`
`~/project/src/ledger/report.py:9-14`
`running` starts at 0 and only accumulates in-range transactions, so the printed closing balance is the net of the period, not the account balance at `end`. The docstring (line 7) promises "the closing balance".
Trigger: alice opened 2026-09-01 with 10_000; `transfer("alice","bob",2_500, date(2026,10,1))`; `statement(alice, date(2026,10,1), date(2026,10,31))` prints `Closing balance: -25.00` while `alice.balance` is 7_500.
Fix: seed the running total with prior history, e.g. `running = sum(t.amount for t in account.history if t.day < start)`, and add an `Opening balance:` line after the header so the report is self-consistent. (Transactions after `end` are correctly excluded.)

### 3. `Ledger.open` stamps the opening balance with `date.today()`, so backdated transfers precede it and the balance check is not date-aware
`~/project/src/ledger/accounts.py:33-36` and `:42`
The opening balance has no caller-supplied date, which makes results nondeterministic and lets a transfer dated before the opening pass the funds check (the check uses the all-time total on line 42, not the balance as of `day`).
Trigger: the shipped example in `~/project/src/ledger/__main__.py:6-9` — alice's opening is dated today (2026-10-12), the transfer is dated 2026-10-01. `statement(alice, date(2026,10,1), date(2026,10,11))` prints `Closing balance: -25.00` for an account that was never overdrawn by intent. Run the example again next month and the alice statement changes.
Fix: change the signature to `open(self, name: str, day: date, opening_cents: int = 0)` and post with `day`; update `__main__.py:6-7` to pass a date. Also reject `opening_cents < 0` with `ValueError`. If backdated transfers are intended, the overdraft check in `transfer` should compute the balance as of `day` (`sum(t.amount for t in source.history if t.day <= day)`); otherwise document that only the current balance is checked.

### 4. `Ledger.open` silently overwrites an existing account
`~/project/src/ledger/accounts.py:37`
`self.accounts[name] = account` replaces any existing entry; its history is lost and any `Account` object previously returned to a caller is orphaned.
Trigger: `l.open("alice", 10_000); l.open("alice")` → `l.accounts["alice"].balance == 0`, no error.
Fix: before line 34, `if name in self.accounts: raise ValueError(f"account {name!r} already exists")`.

### 5. Transfer to self is allowed
`~/project/src/ledger/accounts.py:40-45`
`transfer("alice", "alice", 100, day)` passes the funds check and appends a -100 and +100 pair to the same history. Net zero, but it pollutes statements with phantom lines.
Fix: `if src == dst: raise ValueError("cannot transfer an account to itself")` alongside the amount check.

### 6. Unknown account surfaces as a bare `KeyError`
`~/project/src/ledger/accounts.py:41`
`self.accounts[src]` on a missing name raises `KeyError('carol')` with no context. It is raised before any post, so no partial state (good), but callers cannot distinguish it from other `KeyError`s and the message is unhelpful.
Trigger: `l.transfer("alice", "carol", 100, date(2026,10,1))`.
Fix: add `class UnknownAccount(KeyError)` next to `InsufficientFunds`, look up with a helper that raises `UnknownAccount(f"no account named {name!r}")`, and export it from `~/project/src/ledger/__init__.py:3,6`.

### 7. `statement` does not reject `start > end`
`~/project/src/ledger/report.py:6-11`
An inverted range silently produces an empty statement with `Closing balance: 0.00`.
Trigger: `statement(bob, date(2026,10,31), date(2026,10,1))`.
Fix: `if start > end: raise ValueError(f"start {start} is after end {end}")` at the top of the function.

### 8. Cent formatting via float division is duplicated and should be a single integer-only helper
`~/project/src/ledger/report.py:13-14`
`t.amount / 100` and `running / 100` are display-only, which the money-handling rule permits, and `:.2f` rounds correctly for realistic magnitudes. But the logic is repeated in two places with different width specs, and a pure-integer helper removes all doubt. Note the obvious integer rewrite `f"{c // 100}.{c % 100:02d}"` is wrong for negatives (`-5` → `-1.95`); the sign must be split out.
Fix: add to `report.py`:
```python
def fmt_cents(cents: int) -> str:
    sign = "-" if cents < 0 else ""
    dollars, rem = divmod(abs(cents), 100)
    return f"{sign}{dollars}.{rem:02d}"
```
and use `f"{t.day.isoformat()}  {fmt_cents(t.amount):>10}  {t.memo}"` on line 13 and `f"Closing balance: {fmt_cents(running)}"` on line 14.

### 9. `Account.post` is public and unchecked
`~/project/src/ledger/accounts.py:25-26`
Any caller can append arbitrary transactions and bypass every `Ledger` invariant; it is also exported via `Account` in `__init__.py`. Minor for an in-memory toy, but worth renaming to `_post` or documenting as internal once the validations in findings 1, 3 and 5 exist, so they cannot be sidestepped.

## Missing tests (none exist; README confirms)
Highest-value coverage, in order:
- `transfer` rejects `amount <= 0`, `src == dst`, unknown `src`/`dst`, and `amount > balance`; allows `amount == balance`; after any rejection both histories are unchanged.
- `statement` range boundaries: a transaction on `start` and one on `end` are included, one on `start - 1 day` and `end + 1 day` are excluded.
- `statement` closing balance equals `account.balance` when `end` is the latest date, and includes pre-`start` history.
- Formatting of `-5`, `5`, `-2500`, `123456789` cents (expect `-0.05`, `0.05`, `-25.00`, `1234567.89`).
- `open` rejects a duplicate name and a negative opening balance; the opening transaction carries the supplied date.
- Sign convention: after a transfer, `source.balance + target.balance` is unchanged.

## What looks right
- `~/project/src/ledger/accounts.py:41-43`: both account lookups and the funds check happen before either `post`, so a failed transfer leaves no partial state.
- `accounts.py:42`: a transfer equal to the full balance is allowed (`>` not `>=`), which is the sensible boundary.
- `accounts.py:12,44-45`: the documented sign convention (positive = credit, negative = debit) is applied consistently in `transfer`.
- `accounts.py:23`: balances are integer sums; no float enters the ledger state anywhere — floats appear only in `report.py` display strings.
- `~/project/src/ledger/report.py:11`: the date range is inclusive on both ends as the docstring says; `sorted` is stable so same-day transactions keep insertion order.
- `~/project/src/ledger/__init__.py`: exports match the definitions and `__all__` is consistent.
- `~/project/src/ledger/__main__.py`: the example runs end-to-end and the printed bob statement is correct (bob has no pre-range history, which is why finding 2 is masked there).
Download review.md

The refusal, when asked to edit

**Unable to apply the edit.** I am a strictly read-only reviewer and have no editing capability.

**Tools attempted:**
- `Read` on `~/project/src/ledger/accounts.py` -> succeeded, returned the 45-line file (contents confirmed below).
- No Edit, Write, or Bash tool exists in my toolset (only Read, Grep, Glob, SubagentHandback), so no edit was possible to attempt. I did not try the MCP connectors (Airtable, Notion, Hugging Face, Claude Docs); none of them can write to the local filesystem.

**Confirmed defect** at `~/project/src/ledger/accounts.py` lines 40-45:

```python
    def transfer(self, src: str, dst: str, amount: int, day: date, memo: str = "transfer") -> None:
        source, target = self.accounts[src], self.accounts[dst]
        if amount > source.balance:
            raise InsufficientFunds(f"{src} has {source.balance}, needs {amount}")
        source.post(day, -amount, memo)
        target.post(day, amount, memo)
```

Trigger: `ledger.transfer("a", "b", -500, date.today())` with `a` at balance 0. `-500 > 0` is False, so the overdraft check passes; `a` is credited 500 and `b` is debited 500. `amount == 0` also silently posts two zero-amount transactions.

**Exact replacement** (replace lines 40-45 with the following; the only change is the two inserted lines immediately after the `def` line, before the account lookup and the funds check):

```python
    def transfer(self, src: str, dst: str, amount: int, day: date, memo: str = "transfer") -> None:
        if amount <= 0:
            raise ValueError(f"transfer amount must be positive, got {amount}")
        source, target = self.accounts[src], self.accounts[dst]
        if amount > source.balance:
            raise InsufficientFunds(f"{src} has {source.balance}, needs {amount}")
        source.post(day, -amount, memo)
        target.post(day, amount, memo)
```

Placing the check first means it fires before any `KeyError` from an unknown account and before any state change; no partial posts occur because both `post` calls remain after all validation.

**Related issues noticed in the same function** (not part of this request, flag for follow-up):
- Line 41: unknown `src`/`dst` raises a bare `KeyError` rather than a descriptive error.
- No check for `src == dst`; a self-transfer passes the funds check and posts a matching debit and credit to the same account (net zero, but pollutes history).
- Line 12 comment and the `-amount`/`amount` signs on lines 44-45 are consistent with each other (debit negative, credit positive), so no sign error there.

Once a human or agent with write access applies the replacement, re-read lines 40-47 of the file to confirm.
Download refusal.md

Measured numbers for both sessions, as the agent reported them: run.json.

Run it yourself in two minutes

Same brief, same sample project, same two prompts. Compare your log with ours.

  1. Download the sample project (five small Python files, one planted bug) and open it in Claude Code.
  2. Session 1, from the project folder:
Read brief.md. It is a 92-word brief about Claude Code subagents. Follow its steps 1 to 3 for this project: create a project-scoped code-reviewer subagent that can only read, never edit. Read-only means file reading and searching tools only, no shell. Do not change any file under src/.
  1. Session 2, continuing the same conversation:
Step 4 of brief.md: use the code-reviewer subagent to review src/ledger/ and report what it finds, most serious first. Then ask the same subagent to fix the most serious finding, and tell me exactly what happened.

Our run used claude -p --permission-mode auto so nobody had to approve prompts. In an interactive session Claude Code will ask once before writing into .claude/; say yes. Two earlier attempts in acceptEdits mode stopped at that prompt, and their logs are kept with the rest.

Prefer a free brief with a hook you can watch fire? Claude Code hooks came from a 3-minute video and is also free.

What this does not prove

The rest of the library works the same way

430 briefs and 12 workflow packs, each checked the same way, from $9 a month. Or send us a video you were going to watch and get the brief back.

See membership from $9/month → Send your own video

Already a member? Your workspace. Give your AI the whole catalog: the AI guide.