Module 1: Custom Subagents
3. Tool Restriction for Subagents
3. Tool Restriction for Subagents
Description
In the previous capsule you defined subagents with system prompts that establish their role and work criteria. But a system prompt without tool restrictions is a suggestion, not a contract. You can write "you are a reviewer, only analyze code" in the prompt, and the subagent can decide to edit a file "to demonstrate the fix." A reviewer that edits isn't a reviewer — it's a generic agent with a nice title. Tool restrictions turn suggestions into guarantees.
Claude Code offers two mechanisms for controlling tools: tools (allowlist — only these) and disallowedTools (denylist — all but these). In addition, permission modes define how the subagent handles permission requests, and PreToolUse hooks allow conditional validation — for example, allowing Bash but blocking any command that isn't a SELECT.
This capsule teaches you to use each mechanism, when to choose allowlist vs denylist, how to combine tools with hooks for fine control, and how to apply everything to the reviewer → implementer → tester flow of capsule 05.
Why do restrictions define identity?
Without tool restrictions, a subagent's identity depends on it following instructions. This works most of the time, but it fails in edge cases:
System prompt: "You are a reviewer. Only analyze code. Don't modify files."
Situation: The reviewer finds an obvious typo in a comment.
Without restrictions → It fixes it "because it's trivial." It violated its role.
With restrictions → It can't write files. It reports the typo. It fulfills its role.
In a flow where the reviewer passes its report to the implementer, an unauthorized edit can:
- 🔴 Generate conflicts with the implementer's changes
- 🔴 Break traceability (who changed what?)
- 🔴 Produce unreviewed changes (the reviewer reviewed itself)
Think of restrictions as operating-system permissions. You don't ask the auditor "please don't write." You take away their write permission.
Tools available in Claude Code
| Category | Tool | What it does |
|---|---|---|
| Reading | Read | Reads file content |
Glob | Searches for files by pattern (name, extension) | |
Grep | Searches for text within files | |
| Writing | Write | Creates or overwrites files |
Edit | Modifies existing files | |
| Execution | Bash | Runs terminal commands |
| Delegation | Agent | Delegates to another subagent |
Agent(type) | Delegates to a specific subagent | |
| Web | WebFetch | Fetches content from a URL |
WebSearch | Searches the web |
These tools are the pieces you combine to define the capabilities of each subagent.
The tools field — Allowlist
The tools field in the YAML frontmatter defines an explicit list of allowed tools. Anything not listed is implicitly blocked.
Basic syntax
---
name: reviewer
description: Reviews code without modifying files
tools: Read, Grep, Glob
---
This subagent can read files, search text, and search for files by pattern. It can't write, edit, run commands, or delegate. If it tries to use Write, Claude Code blocks it.
Example: Read-only reviewer
---
name: code-reviewer
description: Analyzes code looking for quality and security problems
tools: Read, Grep, Glob
---
You are a specialized code reviewer. You analyze code and produce a structured report.
## What you review
1. Logic errors and unhandled edge cases
2. Security vulnerabilities (SQL injection, XSS, hardcoded secrets)
3. Violations of project conventions
4. Code smells (long functions, duplication, coupling)
## Report format
For each finding:
- **File:** file path
- **Line:** approximate number
- **Severity:** CRITICAL | WARNING | SUGGESTION
- **Description:** what you found and why it's a problem
The system prompt reinforces the restrictions, but the real guarantee is tools. Even if you delete that line from the prompt, the subagent still can't write.
Example: Tester with only Bash
---
name: tester
description: Runs tests and reports results
tools: Bash, Read
---
You are an automated tester. You run the test suite and report results.
## Your flow
1. Read the test configuration (pytest.ini, pyproject.toml)
2. Run: `python -m pytest --tb=short -q`
3. If there are failing tests, run each one individually for more detail
## Report format
- Total tests: X
- Passed: X
- Failed: X (list with name and cause)
- Coverage: X% (if configured)
If a test fails, it reports the failure — it doesn't try to fix it, because it has neither Write nor Edit.
The disallowedTools field — Denylist
disallowedTools is the inverse of tools: it lists which tools are blocked. The subagent can use all tools except those listed.
---
name: research-assistant
description: Researches code and documentation without modifying the project
disallowedTools: Write, Edit
---
You are a researcher. You can read code, search files, run read
commands, and consult the web. You cannot modify any file in the project.
Without tools defined and with disallowedTools: Write, Edit, this subagent has access to Read, Grep, Glob, Bash, Agent, WebFetch, WebSearch — everything except writing.
Block delegation to specific subagents
---
name: coordinator
description: Coordinates only with reviewer and implementer
disallowedTools: Agent(tester), Agent(Explore)
---
Comparison: tools vs disallowedTools
| Aspect | tools (Allowlist) | disallowedTools (Denylist) |
|---|---|---|
| Philosophy | "It can only do this" | "It can do everything except this" |
| Security | Safer — new tools blocked by default | Less safe — new tools allowed by default |
| Maintenance | Requires updating if Claude Code adds useful tools | Adapts automatically to new tools |
| Best for | Narrow roles (reviewer, tester) | Broad roles (implementer, researcher) |
| Risk | Blocking something the agent needs | Allowing something it shouldn't have |
| Forward-compatible | No | Yes |
Practical rule
Does the subagent need few tools? → tools (allowlist)
Examples: reviewer (Read, Grep, Glob), tester (Bash, Read)
Does the subagent need many tools? → disallowedTools (denylist)
Examples: implementer (all but Agent), researcher (all but Write/Edit)
Is security critical? → Always tools (allowlist)
You can combine both: tools defines the allowed base and disallowedTools removes from that base:
---
name: careful-implementer
description: Implements changes but cannot delegate
tools: Read, Grep, Glob, Write, Edit, Bash
disallowedTools: Agent
---
Delegation control: Agent(agent_type)
The tools field accepts Agent(type) to specify who it can delegate to:
---
name: dev-pipeline
description: Runs the complete development flow
tools: Agent(reviewer), Agent(implementer), Agent(tester), Read
---
You are a development pipeline. Run in sequence:
1. **Review:** Delegate to the reviewer → wait for the report with findings
2. **Implement:** Delegate to the implementer with the findings → wait for confirmation
3. **Test:** Delegate to the tester → wait for the test report
4. **Final report:** Consolidate results from the 3 agents
If it tries Agent(another-agent), Claude Code blocks it. You can also block subagents globally from .claude/settings.json:
{
"permissions": {
"deny": ["Agent(Explore)", "Agent(dangerous-agent)"]
}
}
Permission Modes
Permission modes control how the subagent handles permission prompts — an additional layer on top of tool restrictions.
| Mode | Behavior | Use case |
|---|---|---|
default | Requests permissions normally | Supervised agents |
acceptEdits | Auto-accepts file edits | Trusted implementers |
dontAsk | Auto-denies permission prompts | Read-only agents |
bypassPermissions | Skips all checks | CI/CD automation |
plan | Read-only exploration mode | Reviewers, researchers |
Tools, permission modes, and hooks work in layers:
Layer 1: tools/disallowedTools → Which tools does it have available?
Layer 2: permission modes → How does it handle permissions for those tools?
Layer 3: PreToolUse hooks → What additional validations run?
PreToolUse Hooks — Conditional validation
The problem
tools and disallowedTools are binary — allowed or blocked. Sometimes you need something in between: "Bash yes, but only for SELECT." PreToolUse hooks run a validation script before each tool use.
Anatomy of a hook
---
name: db-reader
description: Runs SELECT queries only
tools: Bash, Read
hooks:
PreToolUse:
- matcher: "Bash"
hooks:
- type: command
command: "./scripts/validate-readonly-query.sh"
---
Execution flow
- The subagent tries to use
Bashwith a command - Claude Code runs
validate-readonly-query.shpassing the tool input as JSON via stdin - Exit code 0 → operation allowed
- Exit code 2 → operation blocked (special Claude Code code)
- Any other exit code → hook error (operation is allowed by default)
Validation script: SELECT only
#!/bin/bash
# scripts/validate-readonly-query.sh
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if echo "$COMMAND" | grep -iE '\b(INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|TRUNCATE)\b' > /dev/null; then
echo "Blocked: Only SELECT queries are allowed" >&2
exit 2
fi
exit 0
The JSON it receives via stdin has the structure:
{
"tool_name": "Bash",
"tool_input": {
"command": "psql -d mydb -c 'SELECT * FROM users WHERE id = 1'"
}
}
Validation script: Write only in src/
#!/bin/bash
# scripts/validate-write-path.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
if [[ -z "$FILE_PATH" ]]; then
exit 0
fi
FILE_PATH="${FILE_PATH#./}"
if [[ "$FILE_PATH" != src/* ]]; then
echo "Blocked: You can only write in src/" >&2
exit 2
fi
exit 0
Subagent with a directory hook
---
name: src-implementer
description: Implements changes exclusively in src/
tools: Read, Grep, Glob, Write, Edit, Bash
hooks:
PreToolUse:
- matcher: "Write"
hooks:
- type: command
command: "./scripts/validate-write-path.sh"
- matcher: "Edit"
hooks:
- type: command
command: "./scripts/validate-write-path.sh"
---
You are an implementer that only modifies files in src/.
Now the "src/ only" restriction doesn't depend on the system prompt — it's enforced by the hook.
Combining tools + hooks — The complete pattern
The most powerful combination: the allowlist defines categories, hooks validate specific operations.
---
name: safe-implementer
description: Implements changes in src/ with restricted commands
tools: Read, Grep, Glob, Write, Edit, Bash
hooks:
PreToolUse:
- matcher: "Write"
hooks:
- type: command
command: "./scripts/validate-write-path.sh"
- matcher: "Edit"
hooks:
- type: command
command: "./scripts/validate-write-path.sh"
- matcher: "Bash"
hooks:
- type: command
command: "./scripts/validate-safe-commands.sh"
---
With a safe-commands script based on an allowlist:
#!/bin/bash
# scripts/validate-safe-commands.sh
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# Only allow specific commands
if echo "$COMMAND" | grep -qE '^(python -m pytest|ruff check|ruff format|git status|git diff)'; then
exit 0
fi
echo "Blocked: Unauthorized command" >&2
exit 2
Command allowlist > command denylist. With a denylist you need to anticipate every dangerous command (and there's always one that slips through). With an allowlist you only define what's legitimate.
Connection to the project
In capsule 05, you'll build the complete reviewer → implementer → tester flow. The restrictions you configure here define each agent's guarantee:
reviewer → tools: Read, Grep, Glob
Guarantee: CANNOT modify your code
implementer → tools: Read, Grep, Glob, Write, Edit, Bash
hooks: Write/Edit only in src/
Guarantee: Only modifies source code files
tester → tools: Bash, Read
Guarantee: CANNOT edit code, only run tests
The coordinator that orchestrates the three:
---
name: dev-coordinator
description: Orchestrates the reviewer → implementer → tester flow
tools: Agent(reviewer), Agent(implementer), Agent(tester), Read
---
It can only delegate to the three agents in the flow. It can't run or edit directly — all execution goes through the specialized agents. Without these restrictions, the flow has no guarantees.
Troubleshooting
Problem 1: "My subagent ignores the tool restrictions"
Cause: Tool names are case-sensitive. tools: read, grep doesn't work — it must be tools: Read, Grep. Also verify that you don't use incorrect names like Bash (the name is Bash).
Solution: Use /agents in Claude Code to inspect the subagent's parsed configuration.
Problem 2: "The hook doesn't block operations"
Cause: Only exit code 2 blocks. Exit code 1 is treated as a hook error (not as a block). Exit code 0 allows.
Solution: Verify that your script uses exit 2 to block. Test manually:
echo '{"tool_input": {"command": "DROP TABLE users"}}' | ./scripts/validate-readonly-query.sh
echo $? # Should print 2
Problem 3: "The hook doesn't receive the JSON correctly"
Cause: jq isn't installed, or the script doesn't read stdin with INPUT=$(cat).
Solution: Verify which jq. The script must read the full stdin before parsing:
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
Problem 4: "My implementer can't write even though Write is in tools"
Cause: A PreToolUse hook for Write may be rejecting the path. Another cause: the permission mode blocks writing.
Solution: Add temporary logging to the validation script to see which path it's trying to write:
echo "DEBUG: Requested path: $FILE_PATH" >&2
Problem 5: "The Bash regex validation is fragile"
Cause: There are infinite ways to run destructive commands that a regex doesn't capture (find -delete, python -c "import os; os.remove('...')", etc.).
Solution: For high security, use a command allowlist instead of a denylist. If the risk is very high, remove Bash from the allowlist entirely.
Exercises
Exercise 1: Configure a read-only reviewer
Create .claude/agents/reviewer.md with a subagent that looks for Python functions without docstrings. Only reading tools, structured report format.
See solution
---
name: docstring-reviewer
description: Finds Python functions without docstrings
tools: Read, Grep, Glob
---
You look for Python functions and classes without docstrings.
## Process
1. Find all .py files with Glob: `**/*.py`
2. Read each file and look for `def` and `class` without an immediate docstring
3. Report each finding
## Format
- **File:** path
- **Line:** number
- **Type:** function | class
- **Name:** name
- **Severity:** WARNING (public) | SUGGESTION (private _)
## Final summary
- Total analyzed / Total without docstring / Coverage percentage
Verification: Run /agents and confirm it appears with Read, Grep, Glob. Invoke it and verify it doesn't try to edit files.
Exercise 2: Directory validation hook
Write scripts/validate-write-path.sh that only allows writing in src/. Exit 0 to allow, exit 2 to block.
See solution
#!/bin/bash
# scripts/validate-write-path.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.filePath // empty')
if [[ -z "$FILE_PATH" ]]; then
exit 0
fi
FILE_PATH="${FILE_PATH#./}"
if [[ "$FILE_PATH" == src/* ]]; then
exit 0
fi
echo "Blocked: Write to '$FILE_PATH' not allowed. Only src/" >&2
exit 2
chmod +x scripts/validate-write-path.sh
# Should allow (exit 0)
echo '{"tool_input":{"file_path":"src/models/user.py"}}' | ./scripts/validate-write-path.sh
echo "Exit: $?"
# Should block (exit 2)
echo '{"tool_input":{"file_path":"tests/test_user.py"}}' | ./scripts/validate-write-path.sh
echo "Exit: $?"
Expected output:
Exit: 0
Blocked: Write to 'tests/test_user.py' not allowed. Only src/
Exit: 2
Exercise 3: Design restrictions for 3 subagents
Design the tool configuration (frontmatter only) for: api-reviewer, api-implementer, api-tester. For each one decide: tools or disallowedTools? Hooks? Justify.
See solution
api-reviewer: Pure allowlist, no hooks needed.
---
name: api-reviewer
description: Reviews API endpoints
tools: Read, Grep, Glob
---
api-implementer: Allowlist + hooks to restrict writing to src/.
---
name: api-implementer
description: Implements and fixes endpoints
tools: Read, Grep, Glob, Write, Edit, Bash
hooks:
PreToolUse:
- matcher: "Write"
hooks:
- type: command
command: "./scripts/validate-write-path.sh"
- matcher: "Edit"
hooks:
- type: command
command: "./scripts/validate-write-path.sh"
---
api-tester: Allowlist + hook to restrict Bash to testing commands.
---
name: api-tester
description: Runs API tests
tools: Bash, Read
hooks:
PreToolUse:
- matcher: "Bash"
hooks:
- type: command
command: "./scripts/validate-test-commands.sh"
---
All three use an allowlist because security matters more than convenience. None of them has Agent — delegation is controlled from the coordinator.
Exercise 4: Advanced hook — Command allowlist
Create a script that only allows: python -m pytest, ruff check, ruff format, git status, git diff. Everything else blocked.
See solution
#!/bin/bash
# scripts/validate-allowed-commands.sh
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if [[ -z "$COMMAND" ]]; then
exit 0
fi
ALLOWED_PREFIXES=(
"python -m pytest"
"ruff check"
"ruff format"
"git status"
"git diff"
)
for prefix in "${ALLOWED_PREFIXES[@]}"; do
if [[ "$COMMAND" == "$prefix"* ]]; then
exit 0
fi
done
echo "Blocked: Unauthorized command. Allowed: pytest, ruff, git status/diff" >&2
exit 2
Test:
chmod +x scripts/validate-allowed-commands.sh
echo '{"tool_input":{"command":"python -m pytest tests/ -v"}}' | ./scripts/validate-allowed-commands.sh
echo "Exit: $?" # 0
echo '{"tool_input":{"command":"rm -rf /"}}' | ./scripts/validate-allowed-commands.sh
echo "Exit: $?" # 2
Exercise 5: Configuration debugging
This subagent has errors. Find them and fix them:
---
name: broken_reviewer
description: Reviews code
tools: read, grep, glob, bash
disallowed_tools: Write, Edit
hooks:
preToolUse:
- match: "Bash"
hooks:
- type: command
command: "./scripts/validate.sh"
---
See solution
Errors:
name: broken_reviewer→name: broken-reviewer(hyphens, not underscores)tools: read, grep, glob, bash→tools: Read, Grep, Glob, Bash(case-sensitive)disallowed_tools→disallowedTools(camelCase)toolsalready excludes Write/Edit — thedisallowedToolsis redundant. Remove it.preToolUse→PreToolUse(PascalCase)match→matcher(correct field name)- A reviewer doesn't need Bash — remove it from the allowlist.
Fixed:
---
name: broken-reviewer
description: Reviews code
tools: Read, Grep, Glob
---
By removing Bash, the hooks become unnecessary. Frequently, the best solution to "how do I validate this tool use?" is "does it actually need this tool?"
Exercise 6: 4-agent system
Design the frontmatter for: coordinator (orchestrates), security-reviewer (finds vulnerabilities), fix-implementer (fixes), security-tester (runs bandit/safety). Include a capabilities table.
See solution
# coordinator.md
---
name: security-coordinator
description: Orchestrates the security review flow
tools: Agent(security-reviewer), Agent(fix-implementer), Agent(security-tester), Read
---
# security-reviewer.md
---
name: security-reviewer
description: Finds security vulnerabilities
tools: Read, Grep, Glob
---
# fix-implementer.md
---
name: fix-implementer
description: Fixes vulnerabilities in src/
tools: Read, Grep, Glob, Write, Edit, Bash
hooks:
PreToolUse:
- matcher: "Write"
hooks:
- type: command
command: "./scripts/validate-write-path.sh"
- matcher: "Edit"
hooks:
- type: command
command: "./scripts/validate-write-path.sh"
---
# security-tester.md
---
name: security-tester
description: Runs security analysis tools
tools: Bash, Read
hooks:
PreToolUse:
- matcher: "Bash"
hooks:
- type: command
command: "./scripts/validate-security-commands.sh"
---
Capabilities map:
| Agent | Read | Grep | Glob | Write | Edit | Bash | Agent |
|---|---|---|---|---|---|---|---|
| coordinator | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (3) |
| security-reviewer | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| fix-implementer | ✅ | ✅ | ✅ | ✅ (src/) | ✅ (src/) | ✅ | ❌ |
| security-tester | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ (security) | ❌ |
Each agent has exactly the tools it needs. The coordinator doesn't run directly — all execution goes through the specialized ones.
Summary
- Tool restrictions turn system-prompt suggestions into enforceable guarantees
tools(allowlist): defines exactly which tools are available — everything else blocked. Use it for narrow roles and high securitydisallowedTools(denylist): blocks specific tools — everything else allowed. Use it for broad roles with few restrictionsAgent(type)controls delegation — a coordinator withAgent(reviewer), Agent(implementer)only delegates to those two- The permission modes (default, acceptEdits, dontAsk, bypassPermissions, plan) control how permission requests are handled
- PreToolUse hooks allow conditional validation — exit code 2 blocks the operation
- Allowlist > denylist for security — especially for Bash
- The tools + hooks combination gives granular control: tools defines categories, hooks validate specific operations
- These restrictions are the foundation of capsule 05's reviewer → implementer → tester flow
Additional Resources
- Create Custom Subagents (Anthropic Docs) — Official subagent documentation with tool restriction and hooks
- Claude Code Hooks Reference — Complete reference for PreToolUse and PostToolUse hooks
- Claude Code Permissions — Permission and security model
- Claude Code CLI Reference — CLI flags including
--permission-modeand--agent - Claude Code Settings — settings.json configuration for permissions deny
- Claude Code Best Practices — Best practices that apply to subagents
- jq Manual — jq reference for parsing JSON in validation scripts
- Principle of Least Privilege (OWASP) — The security principle that underlies restrictions
Next capsule: In capsule 04 you'll learn how subagents communicate with each other. A subagent's output is text — and you need to parse it so the next agent understands it. You'll see structured communication patterns and how to chain subagents in a flow where each one receives exactly the information it needs from the previous one.