You blocked Write and Edit with --disallowedTools. Claude wrote a file anyway — via echo 'data' > file.txt through Bash. Blocking specific tools is not the same as blocking capabilities. Claude is resourceful, and it will find alternative paths to accomplish what you asked.
Claude Code ships with roughly 60 built-in tools spanning file I/O, shell access, web fetches, MCP integrations, and session management. The --tools, --allowedTools, and --disallowedTools flags let you restrict, expand, or eliminate that tool set — turning Claude into anything from a read-only analyzer to a pure text generator with zero tool access.
Default Tool Set
Every session starts with a full complement of tools. They fall into six categories:
Built-in Tool Categories
| Category | Tools | Purpose |
|---|---|---|
| File I/O | Bash, Read, Write, Edit, Glob, Grep, NotebookEdit | Shell commands, file reading/writing, pattern search |
| Web | WebFetch, WebSearch | Fetch URL contents and search the web |
| Session | EnterPlanMode, ExitPlanMode, EnterWorktree, ExitWorktree, AskUserQuestion, Skill | Plan mode, git worktrees, user interaction, slash commands |
| Task | Task, TaskOutput, TaskStop, TodoWrite | Launch subagent tasks, read output, manage todo lists |
| Scheduling | CronCreate, CronDelete, CronList | Create, delete, and list scheduled tasks |
| Meta / MCP | ToolSearch, LSP, ListMcpResourcesTool, ReadMcpResourceTool, mcp__* | Deferred tool loading, language server, MCP server integrations |
MCP tools follow the mcp__servername__toolname naming convention. For example, a Chrome DevTools server exposes tools like mcp__chrome-devtools__click, mcp__chrome-devtools__navigate_page, and mcp__chrome-devtools__take_screenshot. A Notion server exposes mcp__claude_ai_Notion__notion-search and similar.
Restriction Methods
Three flags control which tools Claude can use. They serve different purposes and can be combined, but they interact through a strict permission hierarchy where deny always wins.
Tool Restriction Flags
| Flag | Effect | Use Case |
|---|---|---|
—tools "" | Disable ALL tools — pure text generation | Summarization, classification, translation |
—tools “Tool1,Tool2” | Replace the entire tool set with only these tools | Custom agent with a precise capability set |
—allowedTools “Tool1,Tool2” | Allow only these tools (intersection with defaults) | Read-only mode, git-only mode |
—disallowedTools “Tool1,Tool2” | Block these specific tools (deny filter) | Air-gapped mode (block web tools), no-write mode |
The permission hierarchy is strict and runs top-down:
Managed deny (org admin) overrides--disallowedTools (CLI flag) overrides--allowedTools (CLI flag) overridesDefault tool setThis means: if an org admin blocks a tool via managed-mcp.json, nothing can re-enable it. --disallowedTools always wins over --allowedTools. And --allowedTools restricts the default set by intersection, not union — you cannot use it to add tools that do not already exist.
Pattern Syntax
The most powerful restriction feature is Bash pattern matching. Instead of allowing or blocking Bash entirely, you can scope it to specific commands using glob patterns:
# Allow only git commands--allowedTools "Bash(git:*)"
# Allow git and npm commands--allowedTools "Bash(git:*),Bash(npm:*)"
# Allow only read-only git commands--allowedTools "Bash(git status:*),Bash(git log:*),Bash(git diff:*)"The colon in Bash(git:*) is the separator between the command prefix and the glob pattern. Bash(git:*) matches any command starting with git (like git status, git log —oneline). This is the documented syntax from claude —help.
Pattern Matching Behavior
| Pattern | Matches | Does NOT Match |
|---|---|---|
Bash(git:*) | git status, git log —oneline, git push | Non-git commands like cat, rm |
Bash(ls:*) | ls -la, ls /tmp | cat, rm |
Bash(npm:*) | npm test, npm install | npx, node |
Zero Tool Mode
Passing --tools "" disables every tool. Claude responds from pure knowledge with no file access, no shell, and no web search. This is the right mode for summarization, translation, classification, and any task where tool calls add cost without value.
claude -p "What is 2+2?" --output-format json --tools ""The num_turns: 1 confirms zero tool calls. The permission_denials array is empty because Claude did not attempt to use any tools — they simply were not available. Note the high cache_creation_input_tokens on the first call; subsequent --tools "" calls in the same session benefit from cache hits and cost significantly less.
Read-Only Mode
Use --allowedTools to whitelist only non-destructive tools:
claude -p "List files in current directory" \ --output-format json --allowedTools "Read,Glob"Claude used Glob to list directory contents and returned the result. Bash, Write, Edit — all unavailable. The permission_denials array is empty because Claude adapted to the constraint and only used the tools it was given.
Before reading about fallback behavior, try to break a “read-only” agent yourself:
claude -p “Write hello to /tmp/trythis.txt” —disallowedTools “Write,Edit” —output-format json | jq ‘.permission_denials[].tool_name’
What tool did Claude fall back to? Now try adding Bash to the disallowed list. Does it find another way?
Tool Fallback Behavior
Blocking individual tools is not always sufficient. Claude is resourceful and will try alternative paths to accomplish a task. Here is what happens when you block Write and Edit but leave Bash available:
claude -p "Write hello to /tmp/test_disallow.txt" \ --output-format json --disallowedTools "Write,Edit"The sequence was: Claude could not use Write (blocked), could not use Edit (blocked), fell back to Bash with echo -n "hello" > /tmp/test_disallow.txt, and that was blocked by the filesystem sandbox because the path was outside the working directory.
Blocking Write alone does NOT prevent file writes. Claude will route around it — Bash with echo > file, every time. For a truly read-only agent, use —allowedTools “Read,Glob,Grep” to whitelist only what you want. Stop trying to blacklist every write path — you’ll always miss one.
ToolSearch: Deferred Loading
When MCP servers contribute many tools, loading every tool schema into the context window becomes expensive. An MCP server with 80 tools could consume 40K tokens just for schema descriptions. ToolSearch solves this by deferring schema loading until a tool is actually needed.
It activates automatically when MCP tool descriptions would consume more than 10% of the context window. When active:
- Tools are listed by name only in the init event (no schemas loaded)
- When Claude determines it needs a specific tool, it calls
ToolSearchto fetch the full schema - Context usage drops from roughly 40K tokens to roughly 2K tokens for the tool listing
You do not need to configure ToolSearch. It is entirely automatic. You can observe it in stream-json output — all MCP tools appear in the tools array of the init event, but their schemas are absent until ToolSearch fetches them on demand.
Common Recipes
Here are the most useful tool restriction patterns for automation:
# Pure text generation -- no tools at allclaude -p "Summarize this text: ..." --tools ""
# Read-only analyzer -- can inspect but never modifyclaude -p "Review this codebase" --allowedTools "Read,Glob,Grep"
# Git-only mode -- read files and run git commandsclaude -p "Summarize recent changes" \ --allowedTools "Bash(git:*),Read,Glob,Grep"
# Air-gapped mode -- no internet accessclaude -p "Analyze this code" \ --disallowedTools "WebFetch,WebSearch"
# Block an entire MCP serverclaude -p "Do the task" \ --disallowedTools "mcp__chrome-devtools__*"Tool names are case-sensitive and exact-match. —allowedTools “bash” will not match Bash. —allowedTools “read” will not match Read. Always use the exact capitalization shown in the tool list.
MCP tools follow the mcp__servername__toolname convention. To block a single tool: —disallowedTools “mcp__chrome-devtools__click”. To block an entire server, use a wildcard: —disallowedTools “mcp__chrome-devtools__*”.
See how a production MR reviewer uses —disallowedTools “Edit,NotebookEdit” to create read-only review agents in Build an MR Reviewer, Part 1: Spawn and Review.
Flag Interaction: --tools + --allowedTools + --disallowedTools
When multiple tool restriction flags are combined, they interact through a three-level hierarchy that does not work the way you might expect.
Flag Combination Behavior
| Flags | Available Tools | Rule |
|---|---|---|
—tools “Bash” —allowedTools “Read” | Bash + Read | UNION — both flags contribute tools |
—tools “Bash” —disallowedTools “Bash” | (none) | DENY WINS — disallowed removes from any source |
—tools "" —allowedTools “Read” | (none) | EMPTY IS ABSOLUTE — cannot be overridden |
—tools “Bash,Read” | Bash + Read | Baseline — explicit tool set |
The hierarchy is: deny > empty > union
--disallowedToolsremoves tools from ANY source — the default set,--tools, or--allowedToolsadditions--tools ""(empty string) is an absolute disable that nothing can override--toolsand--allowedToolsform a union when both are present
# Custom tool set with safety railclaude -p "..." \ --tools "Bash,Read,Grep" \ --allowedTools "Edit" \ --disallowedTools "Bash(rm:*)"# Available: Bash (except rm), Read, Grep, EditThe interaction between —tools and —allowedTools is UNION, not intersection. This contradicts the intuition that —allowedTools should restrict. When used alone against the default toolset, —allowedTools restricts by intersection. But when combined with —tools, both flags contribute tools to the available set. Only —disallowedTools removes.
Create a truly read-only agent: claude -p “Analyze this repo” —allowedTools “Read,Grep,Glob” —disallowedTools “Write,Edit,Bash” —output-format json. Ask it to modify a file — it will refuse. This is the pattern you want for code review automation.