[BUG] Claude Science Linux sandbox: "inode number out of range" on NFS inodes above 2^63; one grant blocks all sandboxes
Impact
Salk Institute runs research computing on Linux hosts with NFS home directories and NFS research storage holding petabytes of data. Every NFS inode on our systems is above 2^63. As a result:
- Claude Science cannot be given access to any research data folder on NFS.
- With the default setup ($HOME on NFS), Claude Science cannot run any code at all, because its own conda root is on NFS.
- Once a single NFS folder has been granted, every new sandbox fails, including for work on local folders. The grant is saved with scope "always", so the installation stays broken until the user finds and revokes the grant.
The only workaround is copying data to local disk, which is not practical at our scale. This blocks Claude Science for our Linux users.
Environment
- Claude Science 0.1.62 (release, public)
- Debian, kernel 6.1.0-50-amd64 (host puma); same error seen on SNL cluster hosts oryx and node34
- bubblewrap 0.8.0
- Two separate NFS servers (nadata, ktdata), NFSv3, mount options
rw,nosuid,nodev,noatime,vers=3,rsize=65536,wsize=65536,namlen=255,soft,proto=tcp,timeo=600,retrans=2,sec=sys. Both are NetApp, two different models. - Account: Salk Institute organization
Error
mount manifest: the bind at "/snlkt/bryan/cs-inode-test" has an inode number out of range
mount manifest: the bind at "/nadata/adm/bryan_vol/bryan/bin/cs-inode-test" has an inode number out of range
Root cause
The failing inodes are valid unsigned 64-bit values (ino_t) that are above INT64_MAX (9223372036854775807). The local ext4 inodes are small. The pattern matches an inode being read or validated as a signed int64.
| Path | Filesystem | Inode | Above INT64_MAX? | Result |
| --- | --- | --- | --- | --- |
| /scratch/bryan/cs-inode-test | ext4, local | 15466498 | no | works |
| /home/bryan/bin/cs-inode-test | nfs, nadata | 9232362197981932751 | yes | fails |
| /snlkt/bryan/cs-inode-test | nfs, ktdata | 9252844699440657599 | yes | fails |
Test results (controlled run, 2026-10-14)
Setup: $HOME and the data folder moved to local ext4 (HOME=/scratch/bryan claude-science serve --data-dir /scratch/bryan/.claude-science), a new project with no folders granted, and one test folder granted at a time.
| Step | Result |
| --- | --- |
| Baseline shell (echo $HOME; ls /) and Python (import numpy) | Works |
| Grant and list /scratch/bryan/cs-inode-test (ext4) | Works. Inodes inside the sandbox match the host. |
| Grant /home/bryan/bin/cs-inode-test (nfs, nadata) | Grant accepted, then every shell fails with the error above |
| Grant /snlkt/bryan/cs-inode-test (nfs, ktdata) | Grant accepted, then the error names this bind too |
| Shell touching only the ext4 folder, after the NFS grant | Fails with the nadata bind error. One bad bind blocks all new sandboxes. |
| All grants revoked, new project, baseline shell | Works again |
| Grant /snlkt/bryan/cs-inode-test read-only (returned mode=ro, scope=always) | Same error. Read-only makes no difference. |
| Shell touching only local paths, after the read-only NFS grant | Fails with the same error. A read-only grant also blocks all new sandboxes. |
In the default setup ($HOME on NFS), the same error is reported for the conda root under ~/.claude-science/conda, so no code runs at all. Moving --data-dir alone does not avoid this, because the shared package environment stays under ~/.claude-science.
Steps to reproduce
- On a Linux host with an NFS mount whose inodes are above 2^63 (check with
stat -c %i <path>), create<nfs path>/cs-inode-test/hello.txt. The attachedcs-inode-survey.pydoes this and prints the inodes. - Start Claude Science with $HOME on local disk and open a new project.
- Ask Claude to list the folder with a shell command, and approve the folder access card.
- The shell fails with
mount manifest: the bind at "<path>" has an inode number out of range. - Ask for any other shell command, including on a local folder. It fails with the same error until the grant is revoked in Settings > Permissions.
Other observations
- The error is returned only to the session. It does not appear in
claude-science logs: logs captured before and after the read-only grant and revoke were identical. - A path through a symlink (
/home/bryan/bin) is resolved to its NFS target before the bind is built. - Reading NFS is otherwise fine: Claude Science checked 5,000 folders on NFS for credential files without error. The failure is only in building the sandbox mount manifest.
- A related class of bug exists in Claude Code, where large inode values lose precision as JavaScript numbers and skills or commands are silently dropped as duplicates (anthropics/claude-code #13893, #16095).
Requested fix
- Handle full unsigned 64-bit inode numbers in the Linux sandbox mount manifest, so NFS home directories and NFS research data folders can be used.
- Until then, a bind that fails validation should fail only the sessions that use it, not every sandbox on the installation.
Linux docs do not currently list NFS as unsupported, unlike Windows, which documents that network shares cannot be granted.
Full test results and daemon logs available on request. The survey script used to measure inodes is below.
<details><summary>cs-inode-survey.py</summary>
#!/usr/bin/env python3
"""
cs-inode-survey.py: check whether filesystems hand out inode numbers that are
too large for Claude Science, and create a small test folder on each one.
Usage:
python3 cs-inode-survey.py /scratch/$USER /nadata/snlkt/home/$USER /snlkt/somelab ...
For each path it:
1. Reports filesystem type, source (server:export), and mount options.
2. Creates <path>/cs-inode-test/ with hello.txt (skips creation if not writable).
3. Prints the inode of the folder and the file, and flags values that are
over 2^32 (32-bit) or over 2^53 (largest exact integer in a JavaScript number).
Then, in Claude Science, ask it to list each cs-inode-test folder (see the
report template for the exact prompt) and record which ones fail.
"""
import os
import subprocess
import sys
LIMIT_32 = 2**32 - 1
LIMIT_53 = 2**53 - 1
def findmnt(path):
try:
out = subprocess.run(
["findmnt", "-n", "-T", path, "-o", "FSTYPE,SOURCE,OPTIONS"],
capture_output=True, text=True, check=True,
).stdout.strip()
parts = out.split(None, 2)
return (parts + ["?", "?", "?"])[:3]
except Exception:
return ["?", "?", "?"]
def flag(ino):
if ino > 2**63 - 1:
return "OVER 2^63 (does not fit a signed 64-bit integer)"
if ino > LIMIT_53:
return "OVER 2^53 (JS number cannot hold it exactly)"
if ino > LIMIT_32:
return "over 2^32"
return "ok (32-bit)"
def main(paths):
if not paths:
print(__doc__)
sys.exit(1)
print(f"host: {os.uname().nodename} kernel: {os.uname().release}")
try:
bw = subprocess.run(["bwrap", "--version"], capture_output=True, text=True).stdout.strip()
except FileNotFoundError:
bw = "bwrap not found"
try:
cs = subprocess.run(["claude-science", "--version"], capture_output=True, text=True).stdout.strip()
except FileNotFoundError:
cs = "claude-science not found"
print(f"{bw} {cs}")
print(f"HOME={os.environ.get('HOME')}")
print()
# Always include HOME and the default Claude Science folder.
home = os.path.expanduser("~")
extra = [home, os.path.join(home, ".claude-science")]
for p in extra:
if p not in paths and os.path.exists(p):
paths.append(p)
for p in paths:
p = os.path.abspath(os.path.expanduser(p))
fstype, source, opts = findmnt(p)
print(f"== {p}")
print(f" fs: {fstype} source: {source}")
print(f" options: {opts}")
if not os.path.exists(p):
print(" (path does not exist)\n")
continue
print(f" path inode: {os.stat(p).st_ino:>22} {flag(os.stat(p).st_ino)}")
is_test_target = p not in extra
if is_test_target:
tdir = os.path.join(p, "cs-inode-test")
try:
os.makedirs(tdir, exist_ok=True)
f = os.path.join(tdir, "hello.txt")
with open(f, "w") as fh:
fh.write(f"hello from {fstype} at {p}\n")
print(f" test dir: {os.stat(tdir).st_ino:>22} {flag(os.stat(tdir).st_ino)}")
print(f" test file: {os.stat(f).st_ino:>22} {flag(os.stat(f).st_ino)}")
print(f" created {tdir}")
except OSError as e:
print(f" could not create test dir: {e}")
print()
if __name__ == "__main__":
main(sys.argv[1:])
</details>
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗