anthropics / anthropics/sandbox-runtime
filesystem: allow unlink/rename to be denied inside write-allowed paths
- Dominant language
- TypeScript
- Stars
- 5.2k
- Forks
- 441
- Avg merge
- 1d 19h
- Merged PRs (30d)
- 12
Description
## Summary
A write-allowed path can also be deleted or moved, and there is no opt-in path
to anything narrower. "May edit this file" and "may destroy this file" are one
permission today.
`.git` is the case that cannot be worked around with `denyWrite`, because the
runtime deliberately keeps it writable — `sandbox-utils.js`:
export const DANGEROUS_DIRECTORIES = ['.git', '.vscode', '.idea'];
/** Excludes .git since we need it writable for git operations -
* instead we block specific paths within .git (hooks and config). */
## Reproducer
settings.json — writes allowed in the project, nothing else:
```json
{ "network": { "allowedDomains": [], "deniedDomains": [], "allowUnixSockets": [], "allowLocalBinding": false },
"filesystem": { "allowRead": ["/tmp/x/proj"], "denyRead": [], "allowWrite": ["/tmp/x/proj"], "denyWrite": [] } }
```
```bash
# proj/ is a git repo with one commit
srt --settings settings.json -- sh -c "rm -rf /tmp/x/proj/.git"
ls proj/.git # config hooks <- directory survives
git -C proj log # fatal: not a git repository <- history does not
```
`objects`, `refs`, `HEAD` and `index` are gone. `rm` stopped only when it
reached the denied `.git/hooks` and could not remove a non-empty directory, so
what is left on disk looks like an intact `.git`.
## Notes
- The mechanism is already there, just never fed anything but denies:
`generateMoveBlockingRules` (`macos-sandbox-utils.js:433`) denies
`file-write-unlink` + `file-write-create` for any pattern list; `:522` passes
it only `resolved.denies`; `:542` then re-allows both for every write root
unconditionally. A `filesystem.denyUnlink` list subtracted from
`writeAllowFilters` at `:542` and added at `:522` looks sufficient from
outside.
- `rename` has to be in scope, or the same files are destroyed by a different
syscall. The existing rule already pairs unlink with create for that reason.
- **Linux:** bubblewrap is mount-based (`--ro-bind` gives read-only, not
"writable but not deletable") and seccomp cannot filter on path arguments, so
this is probably macOS-only. Precedent: `allowMachLookup` (#83). A field that
silently did nothing on Linux would be worse than the gap — better to refuse
to start there, as an invalid config already does.
- Measured downstream: over ~330 rounds of a multi-agent run, 66 destructive
commands landed inside a role's own writable tree (34 recursive deletes, 11
`reset --hard`), each one permitted by the sandbox and caught only by a
text-inspecting guard in front of it.
- Longer writeup, with the reasoning and the source of that number:
https://github.com/carlostapiaolguin3-stack/seisin/blob/main/docs/upstream/denyUnlink.md
Contributor guide
No contributing guide indexed for this repository
Research direction
Start by tracing DANGEROUS_DIRECTORIES in sandbox-utils.js and generateMoveBlockingRules in macos-sandbox-utils.js, especially the referenced lines where deny rules are passed and write roots are re-allowed. Verify how filesystem settings are validated and how macOS rules pair unlink with create. Done means a denyUnlink setting protects matching paths inside writable roots, covers rename, and does not silently claim Linux support.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- macos, typescript
- Domain
- operating-systems, security
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100