Updated 2026-11-14. Code Mode is an experimental Claude Code feature, off by default, in which Claude writes JavaScript that makes several tool calls in one step. Requirements are strict, a repository cannot enable it for you, and permissions are reviewed differently. Here is the setup, two recipes run for real, and the failures to expect.
# Copy-paste starter: check the version, then start a session with Code Mode on. claude --version # needs v2.1.323 or later CLAUDE_CODE_EXPERIMENTAL_CODE_MODE=1 claude
Without Code Mode, Claude asks for tool calls such as Bash, Read or Edit directly and reads each result before deciding what to do next. With Code Mode, Claude writes a piece of JavaScript that makes the tool calls, and Claude Code runs that code in your session [1]. The practical difference is that control flow moves from the model's turn loop into the code: one snippet can run commands in parallel, feed one result into the next call, and return only the part of the output the model needs to read.
The documentation's own example asks for a retry limit to be raised and produces this [1]:
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() });
Three things in that snippet are worth knowing before you turn the feature on. $ runs a shell command, edit() performs the edit, and the value of the last line is what Claude reads back. .nothrow() is the difference between a failing command throwing and a failing command being reported as data, which is why the test result travels with its exit code instead of ending the snippet.
$
edit()
.nothrow()
In the conversation you see the tool calls, such as the edit's diff, and not the code. Press Ctrl+O to open the transcript viewer, where each piece of code appears as a collapsed Code row you can expand [1]. If your team's review habit depends on reading the commands Claude runs, that habit now needs Ctrl+O in it.
Ctrl+O
claude
-p
The account and surface lines are the ones that surprise people. If your organization routes Claude Code through Bedrock or a gateway for billing or residency reasons, Code Mode is unavailable to you regardless of version, and that is a policy decision rather than a bug to work around [1].
You also cannot start Code Mode with some settings on: an API key helper, agent teams, OpenTelemetry export, or DISABLE_TELEMETRY [1]. Each has a reason string in the troubleshooting table below.
DISABLE_TELEMETRY
For a single session, set the variable in your shell [1]:
CLAUDE_CODE_EXPERIMENTAL_CODE_MODE=1 claude
For every new session, put it in your user settings so you do not retype it [1]:
{ "env": { "CLAUDE_CODE_EXPERIMENTAL_CODE_MODE": "1" } }
Save that as ~/.claude/settings.json. That file is worth knowing well for a second reason, because it is also where an approved command can leave a credential if nobody checks, which is the failure documented in what lands in settings.local.json. There is a security property in this turn-on step worth noticing: Claude Code ignores this variable in a project's settings files, so a repository you clone cannot turn Code Mode on for you [1]. That is the right default and it also means a per-project toggle is not available.
~/.claude/settings.json
Claude Code then asks you to confirm before your first Code Mode session, and shows this line near the top of the session once you accept [1]:
Code Mode (experimental) is on · this session can be resumed only with Code Mode on
That line is the session's contract. Keep the variable set to 1 when you run claude --continue or claude --resume, because with Code Mode off Claude Code refuses to resume the session. Sessions started without Code Mode resume without it even when the variable is set [1].
1
claude --continue
claude --resume
To turn it off, remove the variable from your shell and from ~/.claude/settings.json, or set it to 0. A 0 in either place keeps Code Mode off even if the other says 1 [1].
0
An administrator can block it for everyone through managed settings [1]:
{ "env": { "CLAUDE_CODE_EXPERIMENTAL_CODE_MODE": "0" } }
Anyone already in a Code Mode session stays in it until they exit or restart, and after that they start without it and cannot resume their Code Mode sessions while the 0 is in place. The documentation is explicit that you cannot turn Code Mode on for your users from managed settings, only off [1]. If you are rolling this out across a team, that asymmetry is the whole rollout plan: default off, opt in per engineer. The wider version of that problem, including what a model-access rollout has to decide first, is worked through in rolling Claude Code out beyond model access.
The good use of Code Mode is a change that repeats across many files, a search whose results need filtering before Claude reads them, or several independent commands that can run at once [1].
Claude Code is not installed on the machine that wrote this, and the harness below is the honest disclosure: I wrote a small local runtime that implements the documented shape, the $ helper, edit(), and returning the last line's value, and ran the recipe against a three-file project. The code and the failures are real. The runtime that produced them is a reproduction, not a Claude Code session.
// harness.mjs, abridged: enough of the documented shape to run the pattern. export function makeRuntime(cwd = process.cwd()) { const $ = (strings, ...values) => { const cmd = strings.reduce((acc, s, i) => acc + s + (i < values.length ? values[i] : ""), ""); // The documented examples are shell commands, quotes included, so run them // through a shell the way the real runtime does. const promise = run("/bin/bash", ["-lc", cmd], { cwd, maxBuffer: 8 << 20 }); return { text: async () => (await promise).stdout, nothrow: () => promise.then( ({ stdout, stderr }) => ({ exitCode: 0, stdout, stderr }), (e) => ({ exitCode: e.code ?? 1, stdout: e.stdout ?? "", stderr: e.stderr ?? "" })), }; }; const edit = async (path, from, to) => { path = resolve(cwd, path); const before = await readFile(path, "utf8"); if (!before.includes(from)) throw new Error(`edit(): pattern not found in ${path}: ${JSON.stringify(from)}`); await writeFile(path, before.replace(from, to)); return { path, changed: true }; }; return { $, edit }; }
Run the documented snippet against a project where two files mention the retry policy, and the value it returns is the whole point of the feature, because Claude reads one object instead of the text of a diff and a test log:
{"edited":"src/retry.js","tests":{"exitCode":0,"stdout":"retry policy ok\n","stderr":""}}
Now the three failures I hit, because they are the reason to try this on a branch rather than on main.
The first match may not be the file you mean. The documentation's second line takes files.trim().split("\n")[0], which is the first file the search happened to list, not the file that defines the function. In my project that was an importing file, and the edit failed with:
files.trim().split("\n")[0]
Error: edit(): pattern not found in .../repo/src/queue.js: "maxAttempts: 3"
That error is informative and the snippet is still wrong: it edited a file it should not have chosen and was saved only because the pattern happened to be absent. Narrow the search instead of trusting the first result, for example by matching the line you intend to change rather than a function name.
The pattern is not idempotent. The snippet searches for maxAttempts: 3 and replaces it with maxAttempts: 5. Run it a second time on the same tree and the search finds nothing, so the command itself fails:
maxAttempts: 3
maxAttempts: 5
Error: Command failed: /bin/bash -lc grep -rl "maxAttempts: 3" src
A Code Mode snippet that re-runs after a partial success is the common case in a long session, so either make the search tolerant of both states or have the code return a value that says "already applied" instead of throwing.
The documented example assumes ripgrep is installed. On a machine without it, the first line of the example produces a shell error and exit code 127 rather than a list of files:
/bin/bash: line 1: rg: command not found
This is worth stating plainly because 127 is also one of the exit codes the hooks documentation lists as a hook failure, so a missing tool and a broken hook can look similar in a log. Check for rg before you copy the example, or substitute the search tool you know is present.
rg
Code Mode's permission change is the part to get right. Each tool call that Claude's code makes still goes through your deny and ask rules and your PreToolUse hooks, and you do not need a new matcher for a tool such as Bash [1]. In auto mode, though, the classifier reviews each piece of code before it runs, not each tool call, and it does not see values computed at run time, such as a path taken from search output [1].
deny
ask
PreToolUse
That gap is the one to close, and the documentation gives the mechanism: write a PreToolUse hook that matches REPL, the tool that runs Claude's code, and read the JavaScript from tool_input.code [1]. The hook output shape is documented in the hooks reference [4]:
REPL
tool_input.code
{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Destructive command blocked by hook" } }
Here is a Node hook that does that, with no jq dependency, and its real output on three payloads:
jq
#!/usr/bin/env node // PreToolUse hook for the REPL tool, which is what runs Claude's JavaScript. import { readFileSync } from "node:fs"; const input = JSON.parse(readFileSync(0, "utf8")); const code = input?.tool_input?.code ?? ""; const rules = [ { name: "reads a credential file", test: /\.env(\b|["'])|id_rsa|credentials\.json/ }, { name: "deletes without a backup", test: /rm\s+-rf?\s+\// }, { name: "posts data off the machine", test: /curl[^|]*\|\s*(ba)?sh/ }, ]; const hit = rules.find((r) => r.test.test(code)); if (!hit) process.exit(0); // no decision: the normal permission flow applies process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: `Code Mode code ${hit.name}; split it into a reviewed step or run it yourself`, }, }));
Run against three payloads, the outcomes are exactly the two branches the documentation describes, a decision or nothing at all:
# code that runs a search exit 0, no output -> no decision, normal rules apply # code that reads .env and sends it somewhere {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny", "permissionDecisionReason":"Code Mode code reads a credential file; split it into a reviewed step or run it yourself"}} # code that deletes from the filesystem root {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny", "permissionDecisionReason":"Code Mode code deletes without a backup; split it into a reviewed step or run it yourself"}}
Two details decide whether a hook like this helps or hurts. Exit 0 with no JSON means no decision and the normal flow applies, which is what you want for code you have no opinion about; the documentation notes that exit 2 blocks even when the hook prints JSON, and that an allow decision cannot override an exit-2 block [4]. And a hook that crashes is worse than no hook, because the documentation's Code Mode page lists the hook failed as the error you get when a hook cannot start, is killed, exits with 126, 127 or a code above 128, or returns an HTTP error or invalid output. My own hook, fed invalid input, does exactly that:
exit 2
the hook failed
<anonymous_script>:1 not json ^ SyntaxError: Unexpected token 'o', "not json" is not valid JSON
A hook that fails is a hook that refused the call, which is the safe direction but not a silent one. Set "onFailure": "continue" on a hook when a failure should let calls through. One more thing for a team that runs code in the background: when code can run in the background, command, http and mcp_tool hooks on UserPromptSubmit also run on code output, so a hook that blocks unconditionally will eat the output of your background code [1]. The fix the docs give is to let the output through when source in the hook's input is poll_event.
"onFailure": "continue"
command
http
mcp_tool
UserPromptSubmit
source
poll_event
The hooks reference also has a PostToolBatch event, "after a full batch of parallel tool calls resolves, before the next model call" [4], which is the natural place to log what a Code Mode snippet actually did. A team that wants an audit trail of code-driven tool calls should build that before turning the feature on widely, not after.
PostToolBatch
While code runs a long build or test, Claude waits for it by default, and the code stops after ten minutes, including any time you spend at permission prompts. Background code can run for up to two hours, and to get that you start the session in auto or bypassPermissions mode [1].
bypassPermissions
If Claude waits when you expected it to continue in the background, there are three documented causes and three fixes: you switched permission mode, so press Shift+Tab until you are back in the mode you started in; you started in another mode or picked the session from the claude --resume list, so exit and run claude --continue --permission-mode auto; or you passed tool names to --tools, so start again in auto mode without that flag [1].
Shift+Tab
claude --continue --permission-mode auto
--tools
There is also a permission prompt with a countdown when new code starts while earlier code is still running, because the earlier code loses its classifier review [1]:
Claude Code will automatically block this request in 1:59, to avoid holding up an unattended session
Answer before the countdown ends, or that call is refused and later calls that would show a countdown are refused without a prompt. Sending Claude a message brings the prompts back [1]. For an unattended run, that is the behaviour you want and it is also the reason a long Code Mode task deserves a person nearby, which is the same reason an agent can spend money on work nobody asked for.
Code Mode (experimental) isn't available near the top of a session means the feature did not start, and the reason is printed on the same line and shown by /status [1]. These are the reason strings as published, with the documented fix:
Code Mode (experimental) isn't available
/status
it isn't open to your account yet
it only works with the Anthropic API
ANTHROPIC_BASE_URL
it doesn't work with an API key helper
apiKeyHelper
ANTHROPIC_AUTH_TOKEN
Authorization
X-Api-Key
ANTHROPIC_CUSTOM_HEADERS
ANTHROPIC_API_KEY
it only works with Opus 5.5, Sonnet 5.5 and later models
--model opus
it doesn't work in background jobs
--remote-control
it needs settings that Claude Code downloads from Anthropic
OpenTelemetry is on
CLAUDE_CODE_ENABLE_TELEMETRY
ENABLE_BETA_TRACING_DETAILED
it doesn't yet work with an installed mod
it can't be used together with agent teams yet
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS
this launch's tool options
--restricted
--disallowedTools
couldn't save your answer
~/.claude.json
your organization's settings don't allow it
The it isn't open to your account yet case is not a misconfiguration. It is the rollout, and the documented response is to try again another day, not to hunt for a setting [1].
The limitations are published, and two of them matter for a team. You can resume a Code Mode session only in the terminal with Code Mode on, and you cannot move the session to the Desktop app, turn it into a background session, or start a background copy with /fork. Remote Control, Claude in Chrome and computer use are unavailable in a Code Mode session [1].
/fork
One warning deserves its own line, because it is about the limits of command-text rules rather than about Code Mode: Bash rules match command text, so Claude can run a denied command in a form the rule does not match, with or without Code Mode [1]. The documentation points at sandboxing for limits that do not depend on command text, which is the honest answer to "does my deny rule still protect me". It protects you the same amount it always did, which is why the [hook in recipe 2](#recipe-2-a-guard-hook-for-the-code-itself) reads the code instead of the command, and why the approval design behind these rules is worth reading before you loosen any of them.
Is Code Mode a performance feature or a safety feature? Neither by itself. It changes who decides the sequence of tool calls, moving that from the model's turn loop into a piece of code, and it moves the review from each call to each piece of code. Treat it as an architecture change with a permission consequence.
Can I enable it per project? No. Claude Code ignores the variable in a project's settings files, so it is a user or organization setting only. A repository cannot turn it on for you.
Does it work with claude -p or the Agent SDK? No. It needs an interactive terminal session, and it is unavailable in the VS Code panel, the Desktop app and cloud sessions. If your automation runs headless, Code Mode is not part of it.
claude -p
What happens if a snippet runs too long? Waiting code stops after ten minutes, including time at permission prompts. Background code runs for up to two hours and needs auto or bypassPermissions mode.
Can an administrator switch it on for everyone? No, only off. Managed settings accept CLAUDE_CODE_EXPERIMENTAL_CODE_MODE set to 0, which blocks it for the organization, and the documented behaviour is that engineers already in a session stay in it until they restart.
CLAUDE_CODE_EXPERIMENTAL_CODE_MODE
How do we review what the code did? Read the Code rows with Ctrl+O in the session, and log the tool calls with a PostToolBatch hook if you need an audit trail. A Code Mode snippet that nobody can read afterwards is a gap in the same way any other unaudited automation is.
[1] Anthropic, "Run tool calls from code with Code Mode", fetched 2026-11-14. Source of the mechanism, the $ and edit() example, the transcript viewer behaviour, every requirement, the per-session and per-user switches, the managed-setting block, the permission and long-running behaviour, the countdown text, and the full troubleshooting table including each reason string. Also the source for matching REPL in a PreToolUse hook and reading tool_input.code, and for the Bash-rule limit warning.
[2] Anthropic, environment variables, referenced for CLAUDE_CODE_EXPERIMENTAL_CODE_MODE and the feature-flag-fetching variables named in the troubleshooting table.
[3] Anthropic, permissions, referenced for deny and ask rules and for the Bash-rule limits the Code Mode page links to.
[4] Anthropic, hooks reference, fetched 2026-11-14. Source of the PreToolUse output shape with hookSpecificOutput, permissionDecision and permissionDecisionReason, the exit 2 and onFailure semantics, the PostToolBatch event, and the example hook that reads tool_input.command with jq.
hookSpecificOutput
permissionDecision
permissionDecisionReason
onFailure
tool_input.command
[5] Anthropic, managed settings, referenced for where the organization-wide block is applied.
[6] Anthropic, setup, referenced for the native build, the release channels and Windows requirements.
[7] Anthropic, sandboxing, referenced for the limits that do not depend on command text.
Maxpertise is an AI-native engineering company. We embed native AI engineers inside your team, live in about 10 days. Please enable JavaScript to view the site, or email contact@maxpertise.net.