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

CategoryToolWhat it does
ReadingReadReads file content
GlobSearches for files by pattern (name, extension)
GrepSearches for text within files
WritingWriteCreates or overwrites files
EditModifies existing files
ExecutionBashRuns terminal commands
DelegationAgentDelegates to another subagent
Agent(type)Delegates to a specific subagent
WebWebFetchFetches content from a URL
WebSearchSearches 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

Aspecttools (Allowlist)disallowedTools (Denylist)
Philosophy"It can only do this""It can do everything except this"
SecuritySafer — new tools blocked by defaultLess safe — new tools allowed by default
MaintenanceRequires updating if Claude Code adds useful toolsAdapts automatically to new tools
Best forNarrow roles (reviewer, tester)Broad roles (implementer, researcher)
RiskBlocking something the agent needsAllowing something it shouldn't have
Forward-compatibleNoYes

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.

ModeBehaviorUse case
defaultRequests permissions normallySupervised agents
acceptEditsAuto-accepts file editsTrusted implementers
dontAskAuto-denies permission promptsRead-only agents
bypassPermissionsSkips all checksCI/CD automation
planRead-only exploration modeReviewers, 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

  1. The subagent tries to use Bash with a command
  2. Claude Code runs validate-readonly-query.sh passing the tool input as JSON via stdin
  3. Exit code 0 → operation allowed
  4. Exit code 2 → operation blocked (special Claude Code code)
  5. 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:

  1. name: broken_reviewer → name: broken-reviewer (hyphens, not underscores)
  2. tools: read, grep, glob, bash → tools: Read, Grep, Glob, Bash (case-sensitive)
  3. disallowed_tools → disallowedTools (camelCase)
  4. tools already excludes Write/Edit — the disallowedTools is redundant. Remove it.
  5. preToolUse → PreToolUse (PascalCase)
  6. match → matcher (correct field name)
  7. 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:

AgentReadGrepGlobWriteEditBashAgent
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 security
  • disallowedTools (denylist): blocks specific tools — everything else allowed. Use it for broad roles with few restrictions
  • Agent(type) controls delegation — a coordinator with Agent(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

  1. Create Custom Subagents (Anthropic Docs) — Official subagent documentation with tool restriction and hooks
  2. Claude Code Hooks Reference — Complete reference for PreToolUse and PostToolUse hooks
  3. Claude Code Permissions — Permission and security model
  4. Claude Code CLI Reference — CLI flags including --permission-mode and --agent
  5. Claude Code Settings — settings.json configuration for permissions deny
  6. Claude Code Best Practices — Best practices that apply to subagents
  7. jq Manual — jq reference for parsing JSON in validation scripts
  8. 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.