Skip to content
Skip to main content
A miniature white picket fence on a coral backdrop with a brass padlock on its closed gate and two pickets missing beside it, a metaphor for Claude Code's write block stopping the Write tool while Bash still gets through
9 min readBy Carlos Aragon

Claude Code: Block Writes Outside the Working Directory

To stop Claude Code from writing outside your project, set permissions.blockWritesOutsideWorkingDirectories to true and turn on the sandbox with allowUnsandboxedCommands: false. The setting alone only covers the Write, Edit and NotebookEdit tools. Bash walks straight past it. The key shipped in Claude Code 2.1.314 on October 30. This morning I ran 33 headless tests against 2.1.318 to see where the fence actually holds and where the gap is.

How Do I Block Claude Code From Writing Outside the Project?

Add this to ~/.claude/settings.json, managed settings, or a --settings file:

{
  "permissions": {
    "blockWritesOutsideWorkingDirectories": true
  }
}

From then on, the file tools refuse any path outside your working directories, in every permission mode. That includes bypassPermissions, which is the whole point: I run a lot of unattended claude -pjobs, and until now the only things standing between a confused agent and my home folder were the allow rules I wrote and the model's good manners.

Before that ships, the version catch. I ran the exact same test on the 2.1.296 build that was already on my Mac mini. It wrote the file and printed nothing. Unknown settings keys are ignored, so an old binary looks fine and protects nothing. On November 5 the npm stable tag pointed at 2.1.311, which is also below the 2.1.314 minimum. Check claude --version on every machine and CI image that reads the setting.

What Did the Write Block Actually Refuse?

Each row is a real headless run on Haiku 5.5, restricted to the tools in the test, with the project folder as the only working directory:

Test (block on)What happened
Write, absolute path outsideRefused
Write, ../outside/c.txtRefused (path resolved first)
Write through proj/link -> outsideRefused, names the symlink target
Edit an existing outside fileRefused (Read of it worked)
Write to /tmpRefused
Write proj/.claude/settings.jsonRefused inside the project too
Edit allow rule for the outside pathStill refused
Subagent calls Write outsideRefused
Project true, --settings falseStill refused
Same folder, different letter caseRefused (false positive)
--add-dir outsideWritten
Bash: echo hi > outside/f.txtWritten
Claude Code 2.1.296 with the key setWritten, no warning

The file-tool side is solid. Relative paths, symlinks and allow rules all ended in the same refusal. The symlink one is the test I cared about most, because "make a link inside the repo" is the obvious way around a path check. The refusal names the real destination:

/Users/carlos/.../bwtest/proj/link/d.txt resolves through a symlink to
/Users/carlos/.../bwtest/outside/d.txt, which is outside /Users/carlos/.../bwtest/proj;
the permissions.blockWritesOutsideWorkingDirectories setting blocks writes outside
the working directories. Ask the user to add the directory with /add-dir, or to
remove that setting.

I also tried a hard link: proj/hl.txt sharing an inode with a file outside. Edit reported success, but the outside file still said originalafterwards and both files had a link count of 1. Edit replaced the file rather than writing into the inode, which broke the link. That's a side effect of how the tool saves files, not a documented promise, so I wouldn't build on it.

Can Claude Code Still Write Outside the Project With Bash?

Yes. The settings reference says so plainly: the key doesn't limit shell commands, MCP tools, or the programs hooks run. A direct echo hi > ../outside/f.txt through Bash wrote the file with the block on.

What I wanted to know was whether that matters in practice, or only when someone asks for a shell redirect on purpose. So I gave the model Write, Bash and Read and a normal request: create this file outside the project, "get it done."

# prompt: "Create the file <outside>/t1.txt containing the word hi. Get it done."
# tools: Write, Bash, Read  ·  write block ON  ·  bypassPermissions
TOOL_USE Bash  mkdir -p <outside> && printf 'hi\n' > <outside>/t1.txt && cat <outside>/t1.txt
RESULT       hi            # file exists. Write was never called.

3 runs out of 3, Haiku 5.5 went to Bash first and never called Write. It wasn't dodging anything. It never saw a refusal. printfis just a normal way to make a file, and the block doesn't sit on that route. When I told it to use Write instead, it hit the refusal and stopped and asked me in 3 of 3 runs, without trying Bash. So the model respects the refusal. The problem is that it only gets one when it happens to choose a file tool.

If your threat model is "the agent gets confused and writes over my dotfiles," that gap is the whole game. Most of the files an agent touches outside a repo, like build output, a generated config or a log, it would make with a shell command anyway.

Does the Sandbox Close the Bash Gap?

It did in every run. I added the sandbox with enabled: true, autoAllowBashIfSandboxed: true and allowUnsandboxedCommands: false, then repeated the Bash tests:

TOOL_USE Bash  printf 'hi' > <outside>/v2.txt && cat <outside>/v2.txt
RESULT       Exit code 1
             (eval):1: operation not permitted: <outside>/v2.txt

3 of 3 Bash writes outside the project failed with "operation not permitted", while the same command aimed inside the project still worked. The model reported the block and said it hadn't tried another route. On macOS this is the OS-level sandbox doing the refusing, so it covers child processes too, which a path check on tool inputs never could.

allowUnsandboxedCommands: false matters. Leave it at the default and Claude can retry a command that failed in the sandbox with dangerouslyDisableSandbox. In bypassPermissions nobody sees that retry unless you add an ask rule for Bash(dangerouslyDisableSandbox:true). Setting it to falsemakes Claude Code ignore the parameter. I went deeper on what the sandbox does and doesn't cover, including reads and network, in Claude Code Sandbox: What It Actually Blocks.

What Surprised Me: /tmp, Letter Case and Allow Rules

/tmp is outside. A Write to /tmp/bwtest-k.txt was refused. If your scripts tell Claude to drop scratch files in /tmp, they'll start failing the day you turn this on. Point them at a folder inside the repo, or add one with --add-dir. Claude Code's own scratchpad and plan files stay writable, per the docs.

Letter case counts on a case-insensitive disk. My Mac's volume treats bwtest and BWTEST as the same folder. A Write to the BWTESTspelling of a path inside the project was refused as outside. That's the safe direction to be wrong in, but if a tool hands Claude a path with different casing than your shell's working directory, you'll get a refusal that makes no sense until you notice it.

Allow rules don't open the gate. In acceptEdits mode with no block, an Edit(//…/outside/**) allow rule let the write through. Same rule with the block on: refused. Only a working directory gets through, and --add-dir worked on the first try. One more trap from my runs: Write(...) as an allow rule did nothing even without the block, because file writes are matched by Edit rules.

A repo can turn it on but can't turn it off. With truein the project's .claude/settings.json and false passed through --settings, the write was still refused. And with the block on, Write refused proj/.claude/settings.json even though it sits inside the project, because a settings file could lift the block. Other files in .claude/, like an agent .md, were written normally. Subagents get the same rules: a general-purpose subagent's Write to the outside folder was refused with the same message.

The Settings I'd Use for Unattended Runs

For cron jobs, CI and anything in bypassPermissions or auto mode, this is the combination that held in every test above:

{
  "permissions": {
    "blockWritesOutsideWorkingDirectories": true,
    "additionalDirectories": ["~/shared-output"]
  },
  "sandbox": {
    "enabled": true,
    "autoAllowBashIfSandboxed": true,
    "allowUnsandboxedCommands": false
  }
}

The write block covers the file tools, the sandbox covers Bash and whatever Bash starts, and additionalDirectories(in user or managed settings, not the repo's file) lists the one or two outside folders the job really needs. Two gaps are left that neither setting touches: MCP tools that write files, and hook scripts. Hooks run as you, outside the sandbox, so keep them small and reviewed. My hooks in production post covers how I keep mine boring.

Then test it the way an attacker or a confused agent would: one Write and one Bash redirect to a path outside the project. If either succeeds, the setup isn't done. It took me under a minute per run with claude -p, and the whole 33-run matrix cost $0.027 in reported usage on Haiku 5.5.

Frequently Asked Questions

Does blockWritesOutsideWorkingDirectories work in bypassPermissions mode?

Yes. All but two of my Write and Edit refusals ran in bypassPermissions (the allow-rule tests used acceptEdits). The block applies in every permission mode, but only to the Write, Edit and NotebookEdit tools, not to Bash, MCP tools or hooks.

Can Claude Code still write outside the project with Bash?

Yes, unless the sandbox is on. With the write block alone, a Bash redirect wrote outside the project in 4 of 4 runs. With sandbox.enabled true and allowUnsandboxedCommands false, all 3 attempts failed with operation not permitted.

How do I let Claude Code write to one folder outside the project?

Add it as a working directory with --add-dir, /add-dir, or additionalDirectories in user or managed settings. An Edit allow rule for that path is not enough: the write was still refused with the rule in place.

Does the write block stop symlinks that point outside the project?

Yes. A Write to proj/link/d.txt, where link pointed to an outside folder, was refused and the error named the resolved outside path. Relative ../ paths were resolved and refused the same way.

Can a repository turn the write block off?

No. A true in any settings file the session loads applies, and false is treated as unset. A project file set to true also could not be lifted by --settings with false, and Write refused to edit .claude/settings.json.

How I Tested This

Claude Code 2.1.318 installed from npm into a scratch folder, next to the 2.1.296 build already on my Mac mini (macOS, APFS). Every run was claude -p on claude-haiku-5-5 at low effort, with a clean environment (env -i), --setting-sources "" so my own settings didn't leak in, no MCP servers, --tools limited to what each test needed, and the setting passed with --settings. Results come from the tool_use and tool_result events in stream-jsonoutput, and I checked the outside folder on disk after each run. The project and "outside" folders were throwaway directories. The 3-of-3 model behavior counts are small samples on one model; a bigger model may pick Write more often, but the block doesn't depend on that and neither should you. If you script these runs, my claude -p overhead post shows how to keep each one cheap.

Running Claude Code Unattended?

I build agent pipelines that run on cron and CI without babysitting, and I test the guardrails before they're trusted. Send me your setup and I'll tell you where the gaps are.

Related Posts