Sandbox auto write-protection of .claude/skills etc. is undocumented and cannot be lifted by allowWrite / Edit allow rules
Summary
The sandbox automatically write-denies <project>/.claude/{skills,agents,hooks,commands,workflows,...} and the corresponding ~/.claude/* paths, in addition to the documented protected set (settings.json at every scope, .mcp.json, symlink targets at protected settings paths). The documentation lists only the latter three. We found no documented mechanism can exempt specific paths from this auto-protection:
sandbox.filesystem.allowWritewith an absolute path to<project>/.claude/skillsdoes not lift it (the path stays read-only), while the same entry for a non-protected path works — the control was measured with the same config, so the mechanism itself is alive.- A
permissions.allowruleEdit(//abs/path/.claude/agents/**)does not lift it either, although the documented behavior ("Edit allow rules … grant write access to specific paths, the same waysandbox.filesystem.allowWritedoes") demonstrably works for non-protected paths (control measured). - Glob patterns in
filesystem.allowWrite/denyWriteare skipped entirely on Linux/WSL (debug log:Skipping glob pattern on Linux/WSL), so extension-scoped grants (e.g..../**/*.md) are impossible on this platform. A trailing/**is normalized away and behaves as a plain directory path.
Results were identical in-session (sandbox filesystem settings hot-reload — which also appears undocumented), in freshly rendered subagent policies, and in brand-new sessions.
Environment
- Claude Code 2.1.226 (native build)
- WSL2 (Ubuntu), bubblewrap sandbox,
sandbox.enabled: true,failIfUnavailable: true
Repro
- In a project with the sandbox enabled, add to
.claude/settings.local.json:
``json``
{
"sandbox": { "filesystem": { "allowWrite": [
"/abs/path/to/project/.claude/skills",
"/abs/path/to/control-dir"
] } },
"permissions": { "allow": [
"Edit(//abs/path/to/project/.claude/agents/**)",
"Edit(//abs/path/to/control-dir2/**)"
] }
}
- From the Bash tool (same session — hot reload applies — or a new session):
````
touch .claude/skills/<existing>/SKILL.md # → Read-only file system
touch .claude/agents/<existing>.md # → Read-only file system
touch /abs/path/to/control-dir/x # → succeeds (allowWrite works)
touch /abs/path/to/control-dir2/x # → succeeds (Edit rule grants sandbox write)
- Add
"/abs/path/to/control-dir3/**/*.txt"toallowWrite→ the path gains nothing on Linux/WSL (inner glob silently skipped), while a trailing-/**entry behaves as a plain directory grant.
Impact
Whenever a merge touches these paths, git merge in the main working tree fails:
error: unable to unlink old '.claude/skills/...': Read-only file system
and after merges the local tree silently keeps stale copies of skills/agents/hooks. We work around this via git worktrees (the protection applies only to the project-dir literal path prefixes, so merges inside worktrees succeed) plus a SessionStart recovery hook — but that is reverse-engineered behavior, not documented contract.
Requests
- Document the actual protected set (it is much larger than the three documented items).
- Provide a supported way to exempt specific paths (e.g.
.claude/skills) from the auto-protection withoutfilesystem.disabled: true. - Document that write-side glob patterns are unsupported on Linux/WSL (and the trailing-
/**normalization).
Related
- #56331 — same shape (
config/auto-added todenyWithinAllow, breakinggit checkoutacross refs) - #40133 (closed/locked) —
.claude/skillsauto-add crashing bwrap on symlink layouts; filing a new issue per its lock notice - #84863 — adjacent: the agent editing its own sandbox settings; note that
filesystem.allowWritechanges take effect mid-session (hot reload), which is relevant to that discussion
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗