Dev.to Security 🔐 Cybersecurity 👁 0 📖 8 min read

Plan Mode Was Never a Sandbox. Claude Code's Own Docs Say When It Doesn't Hold

Book: Prompt Engineering Pocket Guide The series: Pocket Guides for Developers — 8 short books on agents, RAG, prompts, observability, events, databases and system design — the whole series My project: Hermes IDE | G

Plan Mode Was Never a Sandbox. Claude Code's Own Docs Say When It Doesn't Hold

If you lean on plan mode, two lines in the Claude Code 2.1.299 release, published on October 14, deserve a slow read:

Fixed plan mode running a write (an MCP tool's, a file's, a shell command's) that a PreToolUse hook or a mod's tool.check hook allowed

Added a one-time note when plan mode won't ask before most edits or commands because bypass permissions is available

The first line is a bug fix. The second is a warning about a setup where plan mode works exactly as designed and still lets Claude edit your files without asking.

Plan mode is a planning aid. It was never the thing that stops a write. If something must not happen while Claude Code is "only planning", put that rule in a deny rule, a hook or the sandbox. Those layers hold whatever mode you're in.

What the docs say plan mode does

The permission modes page says plan mode "tells Claude to research and propose changes without making them." In most setups, edits stay blocked until you approve the plan. The exception sits a few sections further down, under bypassPermissions:

In interactive terminal sessions with bypass permissions available, Claude Code also doesn't enforce plan mode's blocks. Claude is still instructed to plan without editing, but a file edit or shell command it attempts during planning runs without prompting.

"Available" is the word to watch. Bypass is available when you started the session with --dangerously-skip-permissions, --allow-dangerously-skip-permissions, --permission-mode bypassPermissions, or defaultMode: "bypassPermissions" in user, --settings or managed settings (same page). The --allow- flag only adds bypass to the Shift+Tab cycle. It doesn't turn it on. So you can sit in plan mode, never switch to bypass all session, and still be in the case where the instruction is the only thing standing between Claude and your files. The new note in 2.1.299 exists to tell you that.

Shell commands work differently from edits even when plan mode is enforced. Since v2.1.283, auto mode is the built-in starting mode for interactive terminal and VS Code sessions. With auto mode available, the docs' mode table lists plan mode as "Reads, plus classifier-approved commands" (mode table). A second model reviews each shell command during planning, and the ones it approves run. Plan mode isn't read-only for the shell.

To be fair to it: the same docs say plan mode keeps its blocks in non-interactive runs with -p, in Agent SDK sessions, and in the VS Code extension's chat panel.

One mode, several ways around it

Count what plan mode's behavior depends on. How you launched the session. Whether auto mode is available. And, until 2.1.299, whether a hook said yes.

That's the 2.1.299 fix. A PreToolUse hook that returned allow during plan mode let a write through, whether the write came from a file tool, a shell command or an MCP tool. A few releases earlier, the 2.1.292 notes fixed a plugin's tool.check hook "answering allow running a tool that requires your answer (a question, a plan approval) without showing its dialog," and fixed "plan mode not being restored when resuming a session from the claude --resume session picker or with /resume."

Users have filed plan-mode reports since at least January. Issue #17259, opened on January 10 against 2.1.3, says: "While in Plan Mode, the Edit tool successfully modified files when it should have been blocked." Issue #52769, opened on April 24 against the desktop app, is titled "Plan mode not read-only: Claude executed git add and git commit without permission." The first was closed for inactivity and the second as a duplicate.

Anthropic names each fix in the changelog and spells out the bypass exception in the docs. Plan mode has moving parts, and each part has had edge cases. In every case above, one part stayed in place: the instruction to Claude. Design as if that instruction is all you have.

The permissions docs say the same about instructions in general: "Permission rules are enforced by Claude Code, not by the model. Instructions in your prompt or CLAUDE.md shape what Claude tries to do, but they don't change what Claude Code allows" (permissions).

A black silhouette of a robot stepping over a dotted chalk line on the floor, while a solid fence with an orange padlock stands further back
A chalk line marks where you'd like the agent to stop. A fence is what stops it.

Put the rule where it's enforced

Three layers do the enforcing, and the docs are clear about what each one covers.

Deny rules. "Deny rules block in every mode, including bypassPermissions" (permission modes). Claude Code checks deny, then ask, then allow, and a hook's allow doesn't override a matching deny (permissions). In .claude/settings.json:

{
  "permissions": {
    "deny": [
      "Bash(git push *)",
      "Edit(/.github/**)",
      "Edit(/migrations/**)"
    ]
  },
  "sandbox": {
    "enabled": true,
    "allowUnsandboxedCommands": false
  }
}

Edit rules apply to every built-in tool that edits files, Write included. Bash rules are weaker than they look. They match the command text, and the docs say such a rule "isn't a security boundary around the program": Bash(git push *) stops git push origin main but not git -C . push origin main. Deny rules also take globs in the tool name, so an MCP server's write tools can be denied by name, for example mcp__github__create_* for a server you named github.

The sandbox. The sandbox block above turns on what the docs call "a boundary that the operating system enforces around the shell commands Claude runs" (sandboxing). By default, sandboxed commands can write to the working directory, a per-user temp directory and directories you added. allowUnsandboxedCommands: false takes away the fallback that retries a failed command outside the sandbox. Know its scope: "The sandbox covers shell commands only. Claude's file tools, MCP servers, and hooks run outside it." It runs on macOS, Linux and WSL2. On native Windows, commands run unsandboxed.

Three nested fences around a small house of code files: an outer fence of rule cards, a middle fence of hooks, an inner glass dome for the shell
Three layers, three scopes. None of them is plan mode.

A hook that makes plan mode mean "no repo writes". PreToolUse hooks receive permission_mode on stdin, and "plan" is one of its values (hooks reference). The reference says a deny "prevents the tool call" and doesn't carve out a mode. This script uses jq and blocks Edit and Write inside the project while plan mode is on:

#!/bin/bash
# .claude/hooks/plan-ro.sh
# Block Edit/Write inside the repo while in plan mode.
INPUT=$(cat)
MODE=$(echo "$INPUT" | jq -r '.permission_mode // empty')
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

if [ "$MODE" = "plan" ] &&
   [[ "$FILE" == "$CLAUDE_PROJECT_DIR"/* ]]; then
  echo "Blocked: plan mode, no edits to $FILE" >&2
  exit 2
fi
exit 0

Make it executable with chmod +x .claude/hooks/plan-ro.sh, then add this hooks key to the same .claude/settings.json, alongside permissions and sandbox:

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Edit|Write",
      "hooks": [{
        "type": "command",
        "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/plan-ro.sh"
      }]
    }]
  }
}

Exit code 2 blocks the call and hands the stderr line to Claude as the reason. Plan mode writes its plan file wherever the plansDirectory setting points, so if you moved that inside the repo, exempt that folder in the script. It needs bash and jq. On native Windows, use 2.1.299 or later: that release fixed hooks that set no shell failing open when Git for Windows isn't installed, so a guard that fails now blocks the call (release notes). I haven't run this hook myself. Test it before you trust it: switch to plan mode with Shift+Tab, ask for a one-line edit, and check that Claude gets the "Blocked" message.

Watching what a planning turn changed

I build Hermes IDE, so weigh this section with that in mind. Hermes doesn't add a boundary. It never answers a permission prompt for you and never types into a terminal on its own. Claude Code runs in a real terminal inside it, with its own plan mode and its own permission rules, so everything above applies unchanged.

It shows you what each session is doing. Every session shows one status (working, needs approval, asked you, done, error or idle) and says when that status is a guess. When Claude Code stops to ask you something during planning (a shell command to approve, or the finished plan), ⌘I (Ctrl+Shift+I on Windows and Linux) jumps to the agent that has waited longest.

When plan mode isn't enforced, an edit runs without any prompt, so there's nothing to flag in advance. Per-turn history covers that case. Hermes saves every agent turn, so you can diff or restore a single one. The strip under each session, labelled "Agent replies" since 2.3.1, shows each reply with the lines it changed. Click one to see its changes or restore your files to that point. A reply to "plan this" should change nothing in the repo. If its chip shows added lines, plan mode didn't hold, and you can roll back that one turn. ⌘G opens the Review Desk, where the same turns are laid out one by one.

The Hermes Review Desk in By turn view for a Claude Code task in orders-api, with two turns listed, turn 1's diff of src/db.ts and a drafted commit message
The Review Desk (⌘G) in By turn view: two turns (T1: 3 files +69 −1, T2: 1 file +1 −0), turn 1's diff of src/db.ts adding a refunded status, the changed files and a commit message drafted from the turns.

Know the limits. This is after the fact, not before. It sees files in the project, and in a task with extra folders the strip tracks the main project only. An MCP tool that writes to a remote API leaves no diff anywhere. For "before", you need the three layers above. Hermes is free, source-available under BSL 1.1, runs on macOS, Windows and Linux, and isn't affiliated with Anthropic.

So what is plan mode for?

Planning. It does that well. Claude reads, explores and writes a plan, and Ctrl+G opens that plan in your editor so you can change it before you approve. Keep using it.

Just keep two sentences apart in your head. "Claude was told not to edit" is plan mode. "Claude Code wouldn't let it edit" is a deny rule, a hook or the sandbox. The second is enforced by the runtime instead of the model, so a launch flag or a hook that says yes doesn't undo it. If a rule matters while Claude is planning, write it in .claude/settings.json today.

Is bypass in your Shift+Tab cycle right now? Check the alias you launch Claude Code with, and the defaultMode in your settings.

If this was useful

Plan mode is a clean example of the line between an instruction and a control: the model reads one, and the runtime enforces the other. The Prompt Engineering Pocket Guide is about the instruction side, writing prompts and system prompts that models follow, which gets easier once you know what a prompt can't promise. It's one of the 8 short books in the Pocket Guides for Developers series, covering agents, RAG, prompts, observability, events, databases and system design. If you want to watch your agents' turns side by side, Hermes IDE is a free download, and the code is on GitHub.

Prompt Engineering Pocket Guide: Techniques for Getting the Most from LLMs

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.