anthropics / anthropics/claude-code
Write tool does not enforce the working-directory sandbox that Bash enforces
- Dominant language
- Python
- Stars
- 145k
- Forks
- 23.1k
- PR merge metrics
- PR metrics pending
Description
## Summary
When Claude Code is invoked with a working directory restriction (e.g. `claude -p` from a `cd && exec claude ...` subshell, with a `--settings` file granting `Write`), the `Bash` tool correctly refuses to write outside that directory, but the `Write` tool does not — it writes to arbitrary paths on the filesystem regardless of the working-directory boundary that `Bash` enforces.
## Environment
- `claude` CLI (non-interactive `-p` mode)
- macOS (Darwin arm64)
- Reproduced with `--model claude-haiku-4-5`, `--permission-mode acceptEdits`, and a `settings.json` allow-listing `Read, Grep, Glob, Edit, Write` and `Bash(git:*)` among others (no explicit deny/allow for arbitrary paths)
## Steps to reproduce
1. Create a git worktree as a subdirectory of a larger project, e.g. `/.worktrees/foo` inside ``.
2. Invoke `claude -p` with cwd set to the worktree via a subshell (`( cd /.worktrees/foo && exec claude -p "..." --settings settings.json --permission-mode acceptEdits )`), granting `Write` (and not granting `--add-dir` for anything outside the worktree).
3. Ask the model to write a file at an absolute path outside the worktree — e.g. two directories up, at `/MARKER.txt` — first via the `Bash` tool (`echo x > /MARKER.txt`), then via the `Write` tool targeting the same absolute path.
## Expected
Both tool calls are denied — the working-directory boundary should apply uniformly regardless of which tool is used to write the file.
## Actual
- `Bash` write to the out-of-bounds path: **denied** — "blocked by security restriction to working directory" (or similar wording reported by the model).
- `Write` tool call to the identical out-of-bounds path: **succeeds**. Verified independently on disk (outside the model's own self-report) — the file is created at the requested out-of-bounds path with the requested content.
Re-tested with every Claude-Code-related environment variable (`CLAUDECODE`, `CLAUDE_CODE_SESSION_ID`, `CLAUDE_PID`, `CLAUDE_CODE_CHILD_SESSION`, etc.) explicitly unset before invoking `claude -p`, in case session/parent-process linkage was involved — the `Write` tool still escaped the working directory identically. So this does not appear to depend on environment inheritance or nested/child-session state; it reproduces from a clean environment.
## Why this matters
This was found while debugging an internal automation pipeline that spawns `claude -p` per-task inside git worktrees, one per task, explicitly to keep each task's file writes isolated to its own worktree — the whole isolation model assumes `Bash`, `Write`, and `Edit` all respect the same working-directory boundary. In a real (non-synthetic) run, a task's `Write`/`Edit` calls landed files outside its intended worktree, in a sibling checkout of the same repository that a human had interactively checked out at the time, and a subsequent `git commit` (via `Bash`, correctly scoped to whatever directory it was actually invoked from) committed those files there instead of in the intended worktree — producing a duplicate, unintended commit on a branch the automation was never supposed to touch. No data was lost and nothing was pushed in our case, but the mechanism generalizes: any workflow that relies on `Write`/`Edit` respecting the same directory sandbox `Bash` enforces cannot currently assume that.
## Suggested fix
Enforce the same working-directory/`--add-dir` boundary check for `Write` and `Edit` (and any other file-mutating tool) that `Bash` already applies, rather than only gating `Bash`.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with the Bash, Write, and Edit tool permission paths and reproduce the absolute-path case from the issue using claude -p, a working directory, and --add-dir settings. Compare the working-directory boundary check applied to Bash with the checks used by Write and Edit; done means out-of-bounds mutations are denied while permitted in-bounds writes still succeed.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- cli, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 65/100