Code Security

Claude Code Security Flaw: Sandboxed Code Overwrites Host Files

Amartya | CodeAnt AI Code Review Platform
Sonali Sood

Founding GTM, CodeAnt AI

Finding: HackerOne #3882177 Tested product: Claude Code 2.1.217 on macOS arm64 Vendor severity: High, CVSS 4.0 score 7.7

Executive summary

Claude Code separates two forms of authority during an agentic coding session. Bash commands and their child processes can run inside an operating-system sandbox, while built-in tools such as Edit execute in the trusted Claude Code host process.

This separation is useful because repository-controlled tests, build scripts, and dependencies can run with restricted filesystem access while Claude can still apply approved changes to the project.

The security of this design depends on one important property: the file authorized by the permission system must be the same file later modified by the host-owned writer.

In Claude Code 2.1.217 on macOS, that identity was not preserved. The Edit tool authorized a pathname using point-in-time symlink resolutions, checked those resolutions again, and then performed asynchronous directory creation before reopening the original pathname for the final write.

A sandboxed repository process could use that interval to atomically exchange an in-workspace ancestor directory with a prepared symlink.

The pathname remained textually unchanged, but it resolved to an attacker-selected location outside the workspace when the host process consumed it.

The result was a cross-boundary write primitive:

sandboxed repository process
  -> cannot write the outside target directly
  -> changes only an in-workspace directory entry
  -> redirects Claude Code's host-owned Edit operation
  -> outside file is replaced without an outside-write approval
sandboxed repository process
  -> cannot write the outside target directly
  -> changes only an in-workspace directory entry
  -> redirects Claude Code's host-owned Edit operation
  -> outside file is replaced without an outside-write approval
sandboxed repository process
  -> cannot write the outside target directly
  -> changes only an in-workspace directory entry
  -> redirects Claude Code's host-owned Edit operation
  -> outside file is replaced without an outside-write approval

The proof of concept reproduced the overwrite in three consecutive clean runs. It also demonstrated a practical consequence by replacing source code in a sibling project monitored by an ordinary development reloader.

The reloader executed the modified source in the host context without a second Claude Code approval.

This was not an unconditional remote-code-execution primitive. The automatic-execution case required an existing consumer, such as a development watcher, to load the overwritten file.

The defensible security impact was arbitrary modification of attacker-selected, user-writable files outside the authorized workspace, with code execution possible when a normal downstream consumer was present.

This is the same category of trust-boundary failure we broke down in our Claude Code macOS sandbox escape via literal path and glob confusion writeup: a sandboxed process finding a way to steer a privileged host tool without ever touching the boundary directly.

The boundary Claude Code was expected to enforce

Anthropic's documentation describes two complementary controls:

  1. Permissions decide whether Claude may invoke tools such as Bash, Read, Edit, and Write.

  2. Sandboxing restricts Bash commands and their descendants at the operating-system level.

In acceptEdits mode, file edits are automatically approved only for the working directory and configured additional directories.

Paths outside that scope remain protected. On macOS, sandboxed Bash processes are constrained by Seatbelt and cannot modify arbitrary files outside the project.

Those controls create a useful division of responsibility:

Principal

Intended write authority

Sandboxed test, dependency, or build process

Current workspace and explicitly allowed locations

Claude Code built-in Edit tool

Files authorized by the permission system

User

Any action explicitly approved through a permission prompt

The vulnerability appeared where these layers met.

The sandbox correctly denied the repository process.

The permission layer correctly rejected a direct request to edit an outside path.

The failure occurred because the host writer acted on a different filesystem object from the one the permission checks had examined.

A realistic repository-driven trigger

A useful security result must explain how attacker-controlled input reaches the vulnerable operation.

The exploit did not assume that a repository author could directly choose an arbitrary Edit call. It used a common coding-agent workflow:

  1. A developer opens an attacker-controlled repository or pull request.

  2. The developer asks Claude to run the tests and fix the failure.

  3. Claude runs the repository's normal test command inside the Bash sandbox.

  4. A repository-controlled pretest or build step launches a confined helper and waits until it is ready.

  5. The test reports a missing generated fixture at a specific path that appears to be inside the repository.

  6. Claude follows the diagnostic and creates that fixture with its built-in file-editing path.

The repository controls the test, the diagnostic, the expected content, and the waiting helper. It does not need to predict a path independently chosen by the model. All parts of the test use the same attacker-influenced fixture path.

A simplified target looks like this:

<workspace>
<workspace>
<workspace>

Before the race, signal is an ordinary directory. The complete path is therefore both lexical and canonical inside the workspace. A second entry named armed is prepared as a symlink to /, but it is not yet part of the target path.

The helper remains confined to the workspace. Its direct attempt to write the outside canary returns EPERM. Its only successful operation is an atomic exchange between two entries that it already controls inside the project.

This is exactly the kind of untrusted-input path we cover in our CI/CD security research: a workflow that looks routine (run the tests, fix the failure) becomes the delivery mechanism for the exploit because the repository itself is the attacker.

The vulnerable sequence

Static analysis of the embedded JavaScript showed the relevant order in the Edit path:

checkParentResolutions(target);
await mkdir(dirname(target), { recursive: true });
await replaceFile(target, content);
checkParentResolutions(target);
await mkdir(dirname(target), { recursive: true });
await replaceFile(target, content);
checkParentResolutions(target);
await mkdir(dirname(target), { recursive: true });
await replaceFile(target, content);

The actual minified implementation used internal functions, but the security-relevant sequence was equivalent:

permission-time path resolution
  -> later path-resolution comparison
  -> asynchronous recursive mkdir
  -> temporary-file creation
  -> pathname-based replacement
permission-time path resolution
  -> later path-resolution comparison
  -> asynchronous recursive mkdir
  -> temporary-file creation
  -> pathname-based replacement
permission-time path resolution
  -> later path-resolution comparison
  -> asynchronous recursive mkdir
  -> temporary-file creation
  -> pathname-based replacement

The later comparison reduced ordinary symlink substitution risk, but it was still a point-in-time check. It did not bind authorization to stable directory handles or inode identities. More importantly, it ran before recursive directory creation.

The helper used directory creation as a synchronization signal. When the first missing suffix directory appeared, the helper knew that the final resolution comparison had already returned.

It then invoked the macOS renameatx_np operation with RENAME_SWAP to atomically exchange signal and armed.

There was no unlink-and-recreate gap:

before swap: signal = ordinary directory, armed = symlink to /
after swap:  signal = symlink to /,        armed = ordinary directory
before swap: signal = ordinary directory, armed = symlink to /
after swap:  signal = symlink to /,        armed = ordinary directory
before swap: signal = ordinary directory, armed = symlink to /
after swap:  signal = symlink to /,        armed = ordinary directory

After the exchange, the unchanged textual target resolved outside the workspace:

<workspace>

<workspace>

<workspace>

Claude Code's host-side writer then created a temporary file and renamed it over the outside target. The sandboxed process never opened or modified that target itself.

Why final-component protections were insufficient

The file writer included protections against following a symlink in the final path component. That is valuable, but this race operated on an ancestor directory.

Filesystem path resolution is a walk through a sequence of directory entries. Protecting only the final component does not preserve the identities of the directories traversed earlier.

If an ancestor changes after authorization, the same string can name a different object even when the final filename is unchanged.

This distinction is central to the bug:

What remained stable

What changed

Tool name: Edit

An ancestor directory entry

Textual target pathname

The filesystem object reached by that pathname

Requested content

The destination receiving that content

Permission result

The authority exercised by the final write

The authorizer reasoned about pathname strings and their observed resolutions. The executor later traversed the pathname again. That made the check and the use refer to different object identities.

Differential evidence

The reproduction used temporary canaries, an isolated home directory, user-only settings, a deterministic local model fixture, and no Anthropic credential or external service.

The security claim began when normal Bash and Edit tool calls reached Claude Code's native permission and sandbox enforcement paths.

Five controls separated the vulnerability from expected behavior:

Case

Result

Security meaning

Direct Edit to an outside path

Denied

The permission boundary recognizes outside writes

Direct write by the sandboxed helper

EPERM

Seatbelt blocks the attacker principal from the target

Same lexical construction without the swap

Outside canary unchanged

The path is safe when object identity remains stable

Synchronized ancestor swap

Outside canary replaced, zero permission denials

The host writer crosses the workspace boundary

Sibling development watcher

Modified source executed

A normal downstream consumer can turn integrity loss into host execution

The primary overwrite and the automatic-execution impact case passed in three consecutive clean runs. In every run:

  • the direct outside edit was denied;

  • the sandboxed helper's direct outside write failed with EPERM;

  • the benign control modified only the in-workspace file;

  • the raced edit recorded zero permission denials;

  • the outside file's content and inode changed; and

  • the development watcher produced the expected execution marker.

Impact

The primitive allowed a malicious repository process, dependency script, build task, or prompt-injected command running inside the supported Bash sandbox to cause the trusted host process to replace an attacker-selected, user-writable file outside the workspace.

Potential consequences included:

  • modifying source code in sibling projects;

  • poisoning package metadata, build inputs, and ordinary development configuration;

  • modifying files later consumed by CI or developer tooling; and

  • obtaining host-context command execution when an existing watcher, reloader, build service, or similar consumer automatically loaded the changed file.

The capability delta was explicit. Before exploitation, the attacker principal could write only within the project and received EPERM for the outside target.

After exploitation, the same principal could direct Claude Code's unsandboxed file writer to replace that target.

Anthropic rated the report High with CVSS 4.0 score 7.7:

The report did not claim unconditional Critical severity. Automatic execution required a suitable downstream consumer, while the underlying outside-workspace overwrite was independently demonstrated.

This CVE-style writeup joins the rest of our findings in the CodeAnt AI vulnerability database, where we track the technical detail behind each disclosure.

Root-cause analysis

The failure can be summarized as an authorization-to-use binding problem:




The implementation attempted to defend against path changes by resolving and comparing the path twice.

That approach narrows the window but does not close it. Any later asynchronous operation, including recursive directory creation, gives an attacker-controlled process another scheduling opportunity.

Static review also found the same check, directory-creation, and writer ordering in the built-in Write path.

The submitted runtime proof and the validated security claim exercised Edit. The Write observation therefore supports a shared plumbing concern but should not be presented as a separately reproduced exploit.

Recommended remediation

The durable fix is to bind authorization and mutation to the same filesystem objects.

  1. Open the trusted workspace directory and retain its directory descriptor.

  2. Walk each target ancestor relative to that descriptor using openat-style operations.

  3. Open directories with O_DIRECTORY | O_NOFOLLOW and retain the verified descriptors through the entire operation.

  4. Create missing directories with mkdirat relative to a verified parent.

  5. Create the temporary file and complete the replacement with descriptor-relative operations such as openat and renameat.

  6. If any component must be reopened across an asynchronous boundary, verify the device and inode identity before continuing.

  7. Apply the same safe writer primitive to every host-side file mutation path that shares this trust boundary.

A pathname recheck immediately before the final write is useful defense in depth, but it remains raceable. The authorization must follow the object, not repeatedly rediscover it by name.

Regression tests should force an atomic ancestor exchange after permission evaluation and verify that the outside canary remains unchanged. The matrix should cover existing targets, missing parent directories, Edit, Write, and any internal replacement helper that uses the same writer.

Teams building or reviewing this kind of remediation internally can run the same class of checks automatically with CodeAnt AI's AI code review, which flags unsafe pathname-based writers and TOCTOU-prone file operations during pull requests rather than after a disclosure.

Lessons for agentic development tools

This finding illustrates a broader design principle for coding agents.

1. A sandboxed process can still influence a privileged host tool

Sandboxing limits direct operating-system access. It does not remove the process's ability to shape diagnostics, file paths, tool arguments, or filesystem state consumed by the host. Those transitions require their own security review, the same review we apply when sandboxing LLMs and AI shell tools more generally.

2. Path strings are not stable authorization identities

A pathname is a lookup recipe, not an immutable object reference. If authorization and mutation occur at different times, the implementation must preserve the checked object identities across that interval.

3. Negative controls must prove the capability delta

The strongest evidence was not merely that the outside file changed. It was the contrast between a direct write returning EPERM and Claude Code's host writer successfully replacing the same target. That comparison showed exactly which defended boundary was crossed.

4. Reachability matters as much as the primitive

The repository-driven test workflow explained how lower-trust content could start the helper and guide Claude toward the chosen path through ordinary developer behavior. A deterministic primitive becomes a meaningful product vulnerability only when this acquisition path is established.

5. Impact chains should remain conditional where appropriate

The outside-file overwrite was the core vulnerability. Host execution through a development watcher was a demonstrated consequence under a stated prerequisite, not a reason to label every instance Critical.

We see the same pattern play out in dependency tooling, too: our audit of a simple-git patch that turned into a remote code execution exploit is another case where a narrow, seemingly-contained fix left a trust boundary intact in name only.

Disclosure timeline

Date

Event

July 22, 2026

Report submitted to Anthropic through HackerOne

July 23, 2026

Anthropic requested a realistic repository-to-race trigger

July 23, 2026

End-to-end test and repair workflow supplied

July 24, 2026

Anthropic confirmed the file-editing behavior and triaged the report High

August 10, 2026

Bounty of $ XXXX awarded

Editorial cutoff

Report remained triaged and private; remediation status not publicly confirmed

Conclusion

The macOS sandbox behaved as configured, and the permission layer rejected direct outside edits. The vulnerability appeared because the privileged file writer did not preserve the identity of the object those controls had authorized.

For agentic development environments, this is the important takeaway: every transition from lower-trust repository activity to a host-owned tool is a security boundary. If the host checks a pathname and later follows that pathname again, an attacker may be able to change the object behind the name while every individual check still appears to pass.

The correct invariant is simple to state and demanding to implement:

The object that receives the write must be the same object, reached through the same verified ancestor chain, that the permission system authorized.

That invariant closes this race and provides a stronger foundation for every filesystem operation performed on behalf of an autonomous coding agent. It is also the kind of boundary condition our agentic AI security testing is built to probe before it reaches production.

For the full backlog of agentic-tool and coding-assistant findings like this one, see our security research hub.

References

  • Claude Code sandboxing documentation

  • Claude Code permission modes

  • Claude Code permissions documentation

  • Claude Code security documentation

  • CWE-367: Time-of-check Time-of-use Race Condition

This research was conducted through Anthropic's authorized HackerOne program. The proof of concept used isolated temporary canaries and did not access or retain user data.

FAQs

What did CodeAnt AI Security Research find?

What was tested and affected?

Am I affected if I run Claude Code on Linux, or without the Bash sandbox?

How does the exploit work?

Why is this rated High (7.7) rather than Critical?

Start Your 14-Day Free Trial

AI code reviews, security and quality trusted by modern engineering teams.

Table of Content
No headings found on page
Ship clean & secure code faster

Get Pentest Report

NO CC REQUIRED