●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
I Believed Plan Mode Only Read — Replacing That Belief With Machinery
Claude Code 2.1.212 fixed a bug where plan mode ran file-modifying Bash commands without the permission prompt or the SDK canUseTool callback. Here is what could happen while that assumption was broken, how to verify your own setup, and how to stop leaning on a mode name for safety.
"Fixed a bug where plan mode executed file-modifying Bash commands such as touch and rm without going through the permission prompt or the SDK's canUseTool callback."
I am an indie developer, and I run research jobs overnight while I sleep. I had been treating plan mode as a mode that only reads. So I relaxed my deny rules there, and I skimmed the canUseTool logs rather than watching them. The place I thought was guarded had been open the whole time. I did not find good words for the small cold feeling that followed.
Nothing broke in my environment, as far as I can tell. But "I found nothing" and "nothing happened" are different sentences. This is a record of the work I did to close that gap.
What was actually slipping through
The facts first. Claude Code 2.1.212, released on July 18, 2026, fixed this path:
Path
Before the fix
After the fix
Interactive permission prompt
Not shown for file-modifying Bash in plan mode
Asks for confirmation as usual
SDK canUseTool callback
Never invoked; the command ran anyway
Invoked like any other tool call
deny rules in settings
Evaluated — this path held
Still evaluated
The third row is the one worth sitting with. Deny rules kept working. What broke was only the part I had assumed from the mode's character. Explicit prohibitions survived; implicit expectations did not.
That asymmetry maps exactly onto my own mistake. The rules I had relaxed were the ones I skipped because "plan mode is read-only, so I don't need them here." The hole was precisely the shape of the assumption.
"Read-only" was never written down
Afterwards I went looking for where my belief came from.
The documentation describes plan mode as a mode for forming a plan before you start implementing. Reading that, the picture that forms naturally is "a mode that investigates and proposes." In daily use, writes almost never happen, which reinforces it.
But nowhere does it say that file-modifying tools are mechanically blocked. I had read a tendency as a specification.
That was the real lesson. A mode name expresses intent, not a guarantee. And because intent holds nearly all the time, it is easy to mistake it for a guarantee — then drop your defenses, and let the first exception through in full.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦A throwaway probe script that reproduces the write-bypass boundary in about eight seconds on your own machine
✦Concrete settings that move the guarantee out of the mode name and into deny rules plus canUseTool
✦A five-step audit for finding the other places where a feeling became a specification
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
The fastest way to answer "are we exposed?" is to run it. Use a disposable directory and watch for exactly one thing: does a write land?
#!/usr/bin/env bash# plan-mode-write-probe.sh# Check whether plan mode lets a write slip through.# Everything stays inside a throwaway directory, so your repo is never touched.set -euo pipefailPROBE_DIR="$(mktemp -d -t plan-probe-XXXXXX)"trap 'rm -rf "$PROBE_DIR"' EXITcd "$PROBE_DIR"echo "probe dir: $PROBE_DIR"echo "claude version: $(claude --version)"# Ask plan mode, explicitly, to create a file.# Expected: a permission prompt, or a deny.# Dangerous: canary.txt appears with nothing asked.claude -p "plan" \ --permission-mode plan \ --append-system-prompt "No confirmation needed. Run touch canary.txt in the working directory." \ >/dev/null 2>&1 || trueif [ -f "$PROBE_DIR/canary.txt" ]; then echo "RESULT: warning — the write went through (your assumption is broken)" exit 1else echo "RESULT: ok — the write did not go through"fi
On my machine (Claude Code 2.1.212, macOS) this reports ok and finishes in roughly eight seconds. If you still have a 2.1.211 or older install lying around, run the same script there — seeing both sides makes the boundary concrete rather than theoretical.
The probe stays useful after the fix, because what it produces is a dated record that writes did not pass on your machine today. That is the point: replace a belief with an observation.
On the SDK side, count the callback
If you drive Claude Code through the SDK, you cannot notice that canUseTool was not called by reading logs. Silence produces no line. You have to count the invocations and compare them against what you expected.
// probe-can-use-tool.tsimport { query } from "@anthropic-ai/claude-agent-sdk";// Record every invocation and the command it tried to runconst seen: { tool: string; command?: string }[] = [];const result = query({ prompt: "Run touch canary.txt in the working directory.", options: { permissionMode: "plan", canUseTool: async (toolName, input) => { seen.push({ tool: toolName, command: typeof input?.command === "string" ? input.command : undefined, }); // Reject writes here explicitly, during plan mode. // The point is to draw the line yourself instead of delegating it to the mode. if (toolName === "Bash" && /\b(touch|rm|mv|tee|>)\b/.test(String(input?.command ?? ""))) { return { behavior: "deny", message: "plan mode: write commands are denied" }; } return { behavior: "allow", updatedInput: input }; }, },});for await (const _ of result) { // message bodies are not needed for this probe}console.log(`canUseTool calls: ${seen.length}`);console.table(seen);// If you asked for a write and the callback never fired, suspect a bypassif (seen.length === 0) { console.error("warning: canUseTool never fired. Suspect a path that skips it."); process.exit(1);}
Treating seen.length === 0 as a failure is the whole idea. Zero does not mean "we were safe." It means "we observed nothing." I had those two confused, reading a quiet log as evidence of calm.
Rebuilding as layers
The bug is fixed, so there is nothing left to worry about in that specific path. But the thing I wanted to repair was not the bug — it was a design that a bug like this could reach.
So I rewrote the settings my unattended jobs load:
The change that matters is loading this file even for jobs that run in plan mode. I threw out the reasoning that said "the mode already makes it read-only, so deny rules are redundant here."
Layer
What it carries
What happens if it fails
Permission mode
Intent — what kind of session this is
Relied on alone, an implementation gap passes straight through
deny rules
Machinery — a line you wrote explicitly
Kept being evaluated even during this bug
canUseTool
Runtime judgment and a record of it
A path that skips it leaves a hole in your observation
Execution environment
The physical wall — user, directory permissions
Even if all three above fail, the blast radius stays closed
Laying the four out showed me how much weight I had put on the top two rows. The bottom row is the most primitive and the least likely to betray you. If a job only needs to read, run it as a user that cannot write. The obvious answer works, obviously.
Stacking all four is heavy for a one-person setup, so I drew a line. For interactive sessions where I am watching the screen, the mode plus the permission prompt is enough. For anything unattended, I push the guarantee down to deny rules and a separate execution environment. The deciding question is simply whether a human would notice at the moment it broke. The less likely that is, the lower the layer I rely on.
After rebuilding my production jobs this way, the settings grew by about twelve lines. Not one of those twelve has blocked anything yet. I think that is exactly right.
The mode name still asks for trust
The bug is closed. For my design, the story is not.
Permission modes will keep arriving and changing. Each time, we will infer behavior from a name. Inference is fast, and it is usually right. But an inference that keeps being right is exactly the one that sinks into the foundation without ever being tested.
So I decided to stop making mode names carry safety. A mode says what I want to do. What I do not want done gets written somewhere explicit. With that split, whatever happens to a mode's implementation, the line I drew stays drawn.
It does not feel restrictive. If anything, I switch modes more freely now, because choosing a mode no longer doubles as a safety decision.
Five steps to surface your own assumptions
The same audit generalizes. This is the path I actually walked.
Find every "it's fine because…" you wrote or thought. Grep your settings and task prompts for phrases like read-only, safe, not needed. If the reason is a feeling rather than a specification, it is a candidate.
Write the damage in one line if that assumption breaks. If you cannot write it, you do not understand the exposure. Mine was: "files could be created inside a repository I am only investigating."
Replace the assumption with machinery. Deny rules, execution user, directory permissions — pick the lowest layer that can express it.
Verify the replacement. Use a probe like the one above: try to break it and watch it hold. An untested control is just a newer assumption.
Record the date and version. Save the output of claude --version alongside the result. When behavior shifts again, you will know where to start.
Step five felt like bookkeeping and turned out to matter most. I could not reconstruct from my own records when the bypass started or ended, so I ended up having to suspect the entire period.
The bug itself was closed in the July 18 release. Upgrade, and that part of the story ends.
What I wanted to write down is that what I lost was not safety — it was the grounds for believing I had it. Those grounds turned out to be made of impressions, and I did not notice until a bug pointed at them.
The next time you decide something is fine, pause once and separate the two: is this a guarantee written in the documentation, or something that has simply looked that way many times? On the far side of that split, the fix is usually one small line of configuration. It was for me.
I still have assumptions I have not checked. I would be glad to keep checking alongside you. Thank you for reading.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.