What's broken
tools/sandbox-lint's _normalise() strips a single trailing slash before comparing a .claude/settings.json filesystem path against FORBIDDEN_ALLOW_READ / FORBIDDEN_ALLOW_WRITE (mitigation M.29). A path with two or more trailing slashes, e.g. ~/.ssh//, only loses one, so check_invariants() reports no violation for an allowRead/allowWrite entry that in practice still grants access to a credential directory.
Which layer
tools/sandbox-lint/src/sandbox_lint/__init__.py, _normalise() (line 123) and every invariant check that consumes it through _normalised_set().
How to reproduce
from sandbox_lint import check_invariants
settings = {
"sandbox": {
"enabled": True,
"filesystem": {
"denyRead": ["~/"],
"allowRead": ["~/.ssh//"],
"allowWrite": [],
},
},
"permissions": {"deny": [...]}, # the full REQUIRED_PERMISSIONS_DENY set
}
print(check_invariants(settings)) # []
Expected vs actual
Expected: an allowRead entry of ~/.ssh// is caught the same way ~/.ssh and ~/.ssh/ already are, since it names the same directory.
Actual: check_invariants() returns an empty error list, so the credential-path check silently passes.
Surface area
Same gap applies to denyRead and allowWrite. I have a fix ready (_normalise stripping all trailing slashes instead of one) with regression tests, opening as a PR against this issue.
What's broken
tools/sandbox-lint's_normalise()strips a single trailing slash before comparing a.claude/settings.jsonfilesystem path againstFORBIDDEN_ALLOW_READ/FORBIDDEN_ALLOW_WRITE(mitigation M.29). A path with two or more trailing slashes, e.g.~/.ssh//, only loses one, socheck_invariants()reports no violation for anallowRead/allowWriteentry that in practice still grants access to a credential directory.Which layer
tools/sandbox-lint/src/sandbox_lint/__init__.py,_normalise()(line 123) and every invariant check that consumes it through_normalised_set().How to reproduce
Expected vs actual
Expected: an
allowReadentry of~/.ssh//is caught the same way~/.sshand~/.ssh/already are, since it names the same directory.Actual:
check_invariants()returns an empty error list, so the credential-path check silently passes.Surface area
Same gap applies to
denyReadandallowWrite. I have a fix ready (_normalisestripping all trailing slashes instead of one) with regression tests, opening as a PR against this issue.