[BUG] freebsd linuxator

Status Open
Reported on v2.1.212
Maintainer reply None cached
Activity 5 comments · opened Aug 31, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

When I upgrade claude version to latest claude running in linuxator stopped working because could create temporary files.

What Should Happen?

It should work properly. bash subprocesses should work without issue.

Error Messages/Logs

Claude Code here is the Linux build under linuxulator. The jail's `uname` is FreeBSD,
       so claude.ai/install.sh won't fetch a Linux build. Instead MOUNT the host's
       install (/usr/local/libexec/claude-code) ro and symlink it onto PATH -- it
      runs via linuxulator using the /compat/linux libs. No install step at all.
       CRITICAL: set DISABLE_AUTOUPDATER=1 (in ~claude/.profile, done by the overlay).
      Otherwise Claude's built-in updater silently downloads newer builds into
       ~/.local/share/claude/versions and runs those, shadowing the mount. Versions
       from ~2.1.25x switched from the /dev/fd idiom (works here via the linrdlnk
       fdescfs, #2) to /proc/self/fd, whose fd symlinks resolve EMPTY inside the
       jail's linprocfs -- so mkdir("/proc/self/fd/N/child") returns ENOTDIR and every
       Bash/Write tool call dies at scratchpad creation. (ktrace pinpointed it; the
       host is unaffected because /proc/self/fd works outside a jail.) The updater
      pins to whatever the host claude-code PKG is; if that pkg later moves past the
      /dev/fd versions, mount a specific pinned build instead. First thing to check
      if the jail's Bash tool breaks: `claude --version` in the jail vs the mount.

Steps to Reproduce

Update version to latest and run claude . During any task on linuxator I see that could ran bash because couldn't create temporary file.

Claude Model

Opus

Is this a regression?

Yes, this worked in a previous version

Last Working Version

2.1.212

Claude Code Version

2.1.251

Platform

Anthropic API

Operating System

Other

Terminal/Shell

Other

Additional Information

_No response_

View original on GitHub ↗

4 Comments

dmuszyns · 1 month ago

Independent confirmation, with a tighter bisect and one correction to the scope.

Same failure, but not in a jail

FreeBSD 14.3-RELEASE amd64, security.jail.jailed=0, jls empty — a plain host running the Linux build via the VS Code extension under the linuxulator (linprocfs on /compat/linux/proc, fdescfs on /dev/fd and /compat/linux/dev/fd, /root nullfs-mounted at /compat/linux/root). Every Write/Edit fails with ENOTDIR regardless of target path, and the Bash tool never launches a shell. Reads still work, so the session looks healthy until you try to write or run something.

Worth flagging because the report above says "the host is unaffected because /proc/self/fd works outside a jail" — that isn't true for the traversal this depends on, so please don't scope the fix to jails.

The mechanism is also slightly different from the description: ls /compat/linux/proc/self/fd lists 0 1 2 3 4 … correctly, so the fd symlinks are not empty. What fails is walking through one. fdescfs implements no VOP_LOOKUP beneath its numeric entries, so /proc/self/fd/<N>/<child> returns a genuine in-kernel ENOTDIR:

linux_openat(AT_FDCWD,...,O_RDONLY|O_NOCTTY|O_NONBLOCK|O_LARGEFILE|O_NOFOLLOW)
NAMI  "/proc/self/fd/17/<target>"
RET   linux_openat -1 errno -20 Not a directory

(kdump renders Linux-ABI errnos negative, so grepping traces for errno 20 finds nothing.)

Bisect: 2.1.250 good → 2.1.251 bad

Each version installed on its own, extension host restarted, and the running binary confirmed each time with ps -axo args | grep native-binary/claude (registered ≠ running):

| Version | Result |
|---|---|
| 2.1.240 | works |
| 2.1.245 | works |
| 2.1.248 | works |
| 2.1.250 | works |
| 2.1.251 | broken |
| 2.1.260 | broken |

No 2.1.249 was published, so the regression is exactly 250 → 251. Reverting to 2.1.250 restores full function, so it is the build and not environment drift. Note it is still broken in 2.1.260, nine releases after this issue was filed. ("Last working 2.1.212" in the report is much looser than necessary.)

Three symptom strings, one cause:

  • ENOTDIR: not a directory, mkdir '/compat/linux/tmp/claude-0/<slug>' — Bash scratch dir
  • task output swap refused (open refused a swapped leaf (ENOTDIR)): …/tasks/<id>.output
  • ENOTDIR: not a directory, open '<any path>' — Write/Edit, including paths under the workspace

Failure is near-total but not perfectly deterministic — an occasional lone echo succeeded between failures, so a single good call does not clear a version.

Which change likely did it

Grepping the shipped binaries:

| String | 2.1.248 | 2.1.250 | 2.1.251 | 2.1.260 |
|---|---|---|---|---|
| /proc/self/fd | — | 24 | 39 | — |
| swapped leaf | 0 | 0 | 2 | 2 |
| /dev/fd/ | 3 | 3 | 5 | 5 |

swapped leaf is absent from every working build and present from 2.1.251 on, and /proc/self/fd occurrences jump 24 → 39 across the same boundary. That points at the 2.1.251 symlink-swap/TOCTOU hardening — "Read/Write/Edit could follow a symlink swapped inside the working directory after the permission check" and "Bash output files are now created/read so a sandboxed command cannot redirect or replace them."

The intent is right; re-opening a validated path by descriptor is the correct defence. The portability problem is doing it via a /proc/self/fd/<dirfd>/<name> path string. An openat(dirfd, name, …) / O_PATH-relative form gets the same TOCTOU guarantee and works on FreeBSD, as does the /dev/fd idiom the older builds used.

Ruled out, to save anyone repeating it

  • Mounting fdescfs with -o linrdlnk on /compat/linux/dev/fd — affects readlink(2) only, not traversal, so it rescues the /dev/fd idiom but not this one.
  • stat/statx struct mistranslation; open-flag mistranslation; /compat/linux path double-prefixing.
  • A Node-vs-Bun difference — every version in the table above is Bun, so the runtime is not the variable.
  • The linuxulator generally: stat/S_ISDIR, newfstatat, O_DIRECTORY, the create path, and glibc Linux binaries (sh, touch, mkdir, test -d) all behave correctly.

Workaround

Pin 2.1.250 — "pinned": true in extensions.json, plus per-extension "Ignore Updates".

DominoTree · 20 days ago

We fixed this at the beginning of the month and hopefully should have it MFCed soon

https://github.com/freebsd/freebsd-src/commit/b3f7a438bb233c85290dcdc5241d86c49100b6b7

https://reviews.freebsd.org/D59393

chriswells0 · 7 days ago

@DominoTree - Any idea which releases will get the FreeBSD patch? I'm on 14.4 and hope I won't need an OS upgrade to use Claude.

For the Anthropic devs: going back to 2.1.250 means losing access to Opus 5.5 and Sonnet 5.5, so it's not a "great" workaround. We'd all appreciate getting that 1 small change reverted for a while.

DominoTree · 7 days ago

@chriswells0 We should be able to merge this (and a couple subsequent related fixes) into 15 but I'll need to dig around a bit to see what 14 would look like

Surprised to see so many folks lately still on 14.x although I suppose our users aren't usually known for wanting the "latest and greatest thing" 😁️

Showing cached comments. Read the full discussion on GitHub ↗