Skip to content

Delegating to a subagent with narrower permissions

In this lesson we hand a code review to a second agent that can read and nothing else. A short file defines it, and the main agent delegates the review of a small diff to it. Afterwards we compare what the main conversation holds with the same review done in the main conversation. We also read what the reviewer couldn’t do. The change under review is the clear command from the allowlists lesson, saved as a diff in the repository so everyone reviews the same lines.

The file format, the field names, and the delegation behavior here are Claude Code’s, checked against the vendor’s documentation on the date at the end of the lesson [1]. Other coding agents delegate in other ways. The idea, a narrower agent for a bounded job, transfers.

A subagent is a separate agent instance the main agent delegates a task to. It has a context window of its own and a system prompt of its own. Its tool list and its permission mode can be narrower than the main agent’s. It starts with its own system prompt, the task message the main agent writes for it, the project’s CLAUDE.md files and a snapshot of git status. The main conversation isn’t in it, and neither are the files the main agent has read. The subagent works from the task message, and when it is done its result comes back to the main conversation. Everything the subagent read or searched on the way stays in its own window [1].

That gives two things you can use. The first is context: a task that would fill the main conversation with search results, logs, or file contents runs elsewhere. The result comes back, and the search results don’t. The second is permissions: a subagent’s tool list can be shorter than the main agent’s, so a job that needs only reading runs in an agent that can only read. The blast radius of a review then is the review, whatever the reviewer’s instructions say [1].

Checkpoint · choice

The main agent delegates a search across the repository to a subagent, which reads twelve files and answers. What does the main conversation contain afterwards?

In Claude Code a subagent is a Markdown file with YAML frontmatter, and the body of the file is the subagent’s system prompt. A file in .claude/agents/ in the project defines a subagent for that project, and a file in ~/.claude/agents/ defines one for every project on your machine. Only name and description are required. The main agent uses the description to decide when to delegate, so it says when to use the subagent, and the body says how to do the work [1].

Make a fresh copy of the fixture directory, site/examples/coding-with-agents/first-session/fixture-repo/, outside the course repository, change into it, and give it a first commit, as in the two lessons before this one. Then create .claude/agents/reviewer.md with this content. The same file is in the course repository at site/examples/customizing-agents/subagents/reviewer.md.

---
name: reviewer
description: Reviews a diff and reports findings. Use when asked to review a change before it is committed.
tools: Read, Grep, Glob
model: inherit
---
You review one change and report on it. You never edit files and you never
run commands. Read the diff you are given, then read the files it touches
for context.
Report in this order:
1. What the change does, in one sentence.
2. Findings, one line each, marked `must fix`, `should fix` or `nit`, with
the file and line.
3. What you did not check, and why.
Keep the whole report under 25 lines. Do not quote the diff back.

The line that limits the reviewer is tools: Read, Grep, Glob, its whole tool list. Without a tools line a subagent inherits every tool available to subagents, which in the main agent’s usual setup includes Edit, Write and Bash. With the line, the reviewer can open files and search them, and it has no tool for editing a file or running a command. The sentence “You never edit files” in the body is an instruction, and the tools line is what enforces it. model: inherit runs the reviewer on the same model as the main conversation. The other field you’ll meet is disallowedTools, which removes tools from the inherited list instead of naming the ones to keep. A file may set both. The removals in disallowedTools happen first, and the tools list then picks from whatever survived them, so a tool named in both is gone [1].

Here is that resolution as a small program, over a shortened tool pool. reviewer is the file above, no-writes sets only disallowedTools: Write, Edit, and helper sets neither field. The complete file is site/examples/customizing-agents/subagents/tools.py in the repository.

POOL = ["Read", "Grep", "Glob", "Bash", "Edit", "Write"]
def resolve(pool, tools=None, disallowed=None):
remaining = [tool for tool in pool if tool not in (disallowed or [])]
if tools is None:
return remaining
return [tool for tool in remaining if tool in tools]
DEFINITIONS = {
"reviewer": {"tools": ["Read", "Grep", "Glob"]},
"no-writes": {"disallowed": ["Write", "Edit"]},
"helper": {},
}
Checkpoint · predict

What does this print, one line per subagent?

for name, fields in DEFINITIONS.items():
print(f"{name}: {', '.join(resolve(POOL, **fields))}")

Output verified in CI from site/examples/customizing-agents/subagents/tools.py.

The middle line is the one to read twice. no-writes can’t edit a file with the Edit tool, and it still has Bash, so it can run sed -i or a redirect and change the file that way. A denylist removes what you thought of. An allowlist of tools grants what you thought of and nothing else, which is why the reviewer uses tools. Note one more thing the vendor’s page says about disallowedTools: an entry with a specifier such as Bash(git push *) removes the whole Bash tool from the subagent, and to keep Bash and block one command you write a deny rule in the permissions settings instead, as in the allowlists lesson [1].

The reviewer has no tool for running git diff, so you write the diff to a file it can read. Apply the clear change to your copy. The quickest way is to copy site/examples/customizing-agents/subagents/clear.diff into the copy and run git apply clear.diff, and then git diff shows the change. Save it and look at the size.

Terminal window
git diff > review.diff && wc -l review.diff

Start the agent in the copy with claude --permission-mode default and run /context. It prints how much of the context window is in use and what by [2]. Write down the total. Then send this.

Use the reviewer subagent to review the change in review.diff. Give me its
report as it came back, and then tell me in one line whether you read
review.diff yourself.

You see the handoff in the transcript as one tool call, labeled with the subagent’s name and a few words about the task, reviewer(...) [1]. The report comes back into the main conversation, and the main agent shows it to you. Below is the kind of report the reviewer file above produces. It is illustrative, written for this page and not recorded from a model, and your reviewer’s findings differ in detail.

Prompt (illustrative, not a recorded transcript)

Review the change in review.diff and report as your instructions say.

Response
The change adds a `clear` command that drops every done item from the list and prints how many it removed.
Findings:
- should fix, todo.py:57: `items[:] = kept` mutates the caller's list in place, which the other commands don't do. `main` would work as well with a returned list, and a test that keeps a reference to the old list will be surprised.
- nit, todo.py:56: the message says `item(s)`. `list_items` and `done` don't hedge on plurals, so pick one form.
- nit, test_todo.py:35: the new test covers two done items and one kept. There is no case for an empty list or for a list with nothing done.
Not checked: the pre-existing failure in `test_done_marks_the_numbered_item` is not touched by this diff, and I did not run the tests, because this reviewer has no tool for running commands.

Run /context again and compare the total with the one you wrote down. The difference is what the main agent sent to the reviewer and what came back from it. The diff itself was read in the reviewer’s window, and todo.py and test_todo.py with it. The last line of the main agent’s answer says the same thing in words: it did not read review.diff. To feel the difference, ask the main agent in a fresh session to review review.diff itself, and run /context before and after that too. The whole diff and every file it opened are in the main window this time.

The fixture puts numbers on the two routes with the diff and the report from this page. The complete file is site/examples/customizing-agents/subagents/context.py, next to clear.diff and report.txt, and line_count counts the lines of a file next to it.

Example · run it

Run this from the fixture directory, and compare what you see with the output below.

Terminal window
python3 context.py
Output
diff: 53 lines
report: 8 lines
saved: 45 lines

Output verified in CI from site/examples/customizing-agents/subagents/context.py.

Lines are a rough stand-in for tokens, and the point holds at any scale: the main window took in the report and not the diff. The vendor’s own warning is the other side of the same fact. The result of every subagent returns to the main conversation, so many subagents that each return a long result fill it anyway [1]. The reviewer’s instructions cap the report at 25 lines for that reason.

Now the second comparison, what the reviewer could do. Its tools line gave it three tools. If your reviewer’s report says it ran the tests, it didn’t, and its transcript doesn’t contain a command. When the reviewer finishes, its row under the prompt disappears and the footer shows a /tasks hint for 30 seconds. Run /tasks within that time and press Enter on the reviewer to open its transcript. After that, ask the main agent what tools the reviewer used [1].

Checkpoint · sort

Place each action where it belongs.

Checkpoint · choice

A subagent file sets permissionMode: default. The main conversation was started with --permission-mode acceptEdits. Which mode does the subagent run in [1]?

Exercise

In a fresh copy of the fixture with its own first commit, create .claude/agents/reviewer.md from this lesson, apply clear.diff, and save git diff to review.diff. Run git status --short and keep the output: two modified files, and .claude/, clear.diff and review.diff untracked. Start the agent in Manual mode, note the /context total, ask it to use the reviewer subagent on review.diff and to say whether it read the file itself, then note /context again. In a second fresh session, ask the main agent to review review.diff with no subagent and note /context before and after that too. The result is two before-and-after pairs and the reviewer’s report. The point is to see with your own numbers that the diff never entered the main window, and to read the report knowing what the reviewer could not have done.

A good result: the reviewer’s report has a summary, findings with file and line, and a line on what it did not check; the main agent says it did not read review.diff; the delegated pair grew by less than the direct pair; and git status --short after the delegation prints the same lines it printed before it, with git diff --stat unchanged too. Reflection: which task in your own work last week produced a long output you read once and never needed again?

Stretch: Add a second subagent, a test runner with `tools: Read, Bash` and a body that says to run the tests and report only the failures, and have the main agent chain the two: review first, then tests. Read what came back from each.

Recap

  1. A subagent is a separate agent instance with its own context window, system prompt, tool list and permission mode. It starts with that prompt, the task message, the CLAUDE.md files and a git status snapshot, and without the main conversation. Its result comes back, and what it read doesn’t [1].
  2. In Claude Code a subagent is a Markdown file in .claude/agents/ or ~/.claude/agents/, with name and description required and the body as its system prompt. tools names what it may use, disallowedTools removes from what it inherits, and without tools it inherits every tool [1].
  3. The body asks and the tools line enforces. A reviewer with tools: Read, Grep, Glob has no tool for editing or running, whatever its report says it did.
  4. /context before and after a delegation shows the saving: the diff and the files it touched were read in the reviewer’s window, and the main window took in the report [2].
  5. A subagent’s permissionMode applies when the main conversation is in default, dontAsk or plan. In acceptEdits, auto or bypassPermissions the subagent runs in the main conversation’s mode, and a subagent that declares bypassPermissions gets the main conversation’s mode instead [1].

You can now

  • Sets permissions to the least the work needs

  1. Anthropic. Create custom subagents. Claude Code documentation. Reference. Claude Code subagents
  2. Anthropic. Explore the context window. Claude Code documentation. Reference. Claude Code context window