Max Narditdata & ai systems
← Articles
6 mindev

Claude Code Code Mode: auto mode now reviews the script, not each tool call

In a Code Mode session the auto mode classifier reads the script once and never the calls inside it, so a path or host computed at run time gets no review from it. A PreToolUse hook that can't start or times out now refuses the call, but one that exits 1 still lets it through. The checks that see real values are your rules, your hooks and the sandbox.

Contents

Claude Code 2.1.323, released on November 12, adds Code Mode, an experimental opt-in. Normally Claude asks for one tool call at a time, reads the result and decides what to call next. In Code Mode it writes a piece of JavaScript that makes the calls itself, and a tool called REPL runs that code in your session. This is the docs’ own example, for raising a retry limit:

javascript
const files = await $`rg -l "function retryPolicy" src`.text();
const target = files.trim().split("\n")[0];
await edit(target, "maxAttempts: 3", "maxAttempts: 5");
({ edited: target, tests: $`npm test -- retryPolicy`.nothrow() });

The docs pitch it for work with many related tool calls: one change across many files, a search to filter before Claude reads it, commands that can run in parallel. You see the tool calls in the conversation, not the script; the code sits behind Ctrl+O in collapsed Code rows. Read the permission section before you turn it on, because auto mode’s classifier now checks less and a broken hook behaves differently. The facts below come from the docs and the release notes. I haven’t run Code Mode, so nothing here is a measurement.

What does Claude Code Code Mode change about permission checks?

In auto mode the classifier reviews each piece of code once, before it runs, and not the tool calls inside it. The docs spell out the cost: it “doesn’t see values computed at run time, such as a path taken from search output, so keep rules or hooks for calls you must control.” Every call the code makes still goes through your deny and ask rules and your PreToolUse hooks.

The permission-modes page shows how much the classifier leans on literal values. By default it blocks rm -rf "$VAR" when the variable isn’t assigned anywhere it can see, because it “can’t tell what the command would delete”, and the block clears once Claude re-runs the delete with the real path written into the command. In Code Mode there is no later call where that path shows up for review: the classifier read the script, and the script held a variable. (The edit in the example above wouldn’t have reached the classifier anyway. Edits inside your working directory are auto-approved in auto mode, Code Mode or not.)

In the other permission modes the code starts with no review, and you get the usual prompt for each call that would prompt in that mode. Under bypassPermissions there is no classifier and almost no prompt, so your rules and hooks are all that stands in the way. To check the script itself, add a PreToolUse hook matching REPL and read the JavaScript from tool_input.code. That hook reads the same text the classifier did, with the same blind spot. A hook on Bash or Edit is different: it fires on each call from the code, with the resolved argument in its input. I take that from the docs’ statement that every call goes through hooks; I haven’t tested it.

How do Claude Code PreToolUse hooks behave in Code Mode?

They refuse in more cases than before, though not in all. Outside Code Mode, a command or http hook that can’t start, crashes or times out is a non-blocking error, and the call continues through the normal permission checks unless you set "onFailure": "block". I tested that default for the hooks howto: a hook pointing at a missing script let the call run.

In Code Mode, on a call from Claude’s code, a PreToolUse hook that couldn’t start, was killed, exited with 126, 127 or a code above 128, returned an HTTP error or invalid output refuses the call, with onFailure unset. The error reads the hook failed. A timeout refuses too, with the hook did not finish, and that rule reaches past the code: once Claude has run code in the session, a hook that times out on any tool call refuses it.

Two cases still go through. A hook that exits with an ordinary code such as 1, which is what an uncaught Python exception does, still lets the call through unless you set "onFailure": "block". A guard that breaks internally and still exits 0, like the jq guard from the howto, passes as before. For command and http hooks, "onFailure": "continue" brings back the old behavior.

Prompt filters change too. In a session where code can run in the background, command, http and mcp_tool hooks on UserPromptSubmit also run on the code’s output, with source set to poll_event. A filter written for what you type can now withhold what your build printed.

What happens to an unattended Claude Code session in Code Mode?

Code waits in the foreground for up to ten minutes, prompts included, then stops. Background code can run for up to two hours, but only in a session started in auto or bypassPermissions mode.

If Claude starts new code in auto mode while earlier code still runs, the earlier code loses its review, and Claude Code asks you about its remaining calls that would need one. With no IDE connected, the prompt counts down; the docs’ sample reads “Claude Code will automatically block this request in 1:59, to avoid holding up an unattended session”. Miss it, and that call is refused. So is every later call that would show a countdown, without a prompt, until someone sends Claude a message.

Refusing beats running unreviewed calls, so for a session nobody watches I’d call that the right failure. What the docs don’t say is how a refusal is reported beyond the error Claude gets back. Until someone tests it, I’d assume what I found with denyInsteadOfPrompting: the interactive session went back to idle, nothing raised an error, and the refusal was recorded only in the session itself unless a hook wrote it down somewhere else.

Not that many sessions qualify. Code Mode runs only in an interactive claude session in a terminal, on a direct connection to Anthropic with a supported model: no -p, no Agent SDK, no background sessions or Remote Control, no gateway or cloud provider, no zero data retention, and not alongside a mod that checks tool calls. The docs list the rest.

Who controls whether Claude Code Code Mode is on?

You do, with CLAUDE_CODE_EXPERIMENTAL_CODE_MODE=1 in your shell or in ~/.claude/settings.json. Claude Code ignores the variable in a project’s settings, so a repository you clone can’t turn it on. An organization can block Code Mode with 0 in managed settings, effective for each user after their next restart, but it can’t force it on.

Should you trust Claude Code auto mode with Code Mode on?

In a Code Mode session the classifier checks less than it does outside one. It judges what a script means to do, never the values the script will produce, and for a delete or an upload the target is what decides. When I ranked egress controls by what the model can’t argue past, that ranking had no classifier in it: the sandbox allowlist first, then my own hook, then the built-in mod, then a CLAUDE.md line. I’d put a classifier below the hook. As I wrote in the hooks piece, a model in the gate is weighing, not enforcing, the case the approval-gate essay makes at length. Code Mode doesn’t change that order. It makes the top of it matter more, because the layer in the middle now reads less.

The same release changed what happens when a hook can’t answer. I read the two changes together, though the docs list the second under limitations, not as a safety feature. Code Mode is also two days old: release 2.1.325 fixed scripts that ignored a worktree entered with EnterWorktree, words typed with a Yes at a permission dialog not reaching Claude, and Claude starting work on its own after resuming a session that ended mid-script.

If you set CLAUDE_CODE_EXPERIMENTAL_CODE_MODE=1 this week, do two things first. List the calls that must never happen and note what stops each one. For a shell command that should be the sandbox, because a Bash deny rule reads command text and can be dodged with or without Code Mode. For a file tool, a deny rule or a hook on that tool. Auto mode should be on none of those lines. Then go through your PreToolUse hooks: put "onFailure": "block" on every guard, so a crash that exits 1 refuses too, and "onFailure": "continue" on anything that only logs or notifies, so a timeout there can’t refuse work. Raise timeout only where you set a short one yourself.

01 / Discussion

No comment section here — all discussions happen on X.

Max Nardit@mnardit

02 / More articles