Running Claude Code on FreeBSD via the Linuxulator
Claude Code ships as a Linux ELF binary (bun-based, statically linked against glibc). FreeBSD’s Linuxulator can run it directly — no Linux VM, no Docker, no jail strictly required. This post walks through the host-only setup. If you’d rather sandbox it in a jail, see the companion post on ephemeral Claude jails.
Tested on FreeBSD 15.0-RELEASE amd64. Should work on 14.x without changes.
TL;DR
doas pkg install -y emulators/linux_base-rl9
doas sysrc linux_enable=YES
doas service linux start
curl -fsSL https://claude.ai/install.sh | bash
~/.local/bin/claude --dangerously-skip-permissions
If that “just worked” — great, stop reading. The rest of this post explains what’s actually happening and the one trap that breaks it.
What the Linuxulator gives you
Three pieces have to be in place before a Linux ELF will run:
- The
linux64kernel module — a Linux ABI shim that translates Linux syscalls into FreeBSD ones. Loaded byservice linux start. linux_base-rl9— a Rocky Linux 9 userland mounted under/compat/linux. Providesld-linux-x86-64.so.2, glibc, and the handful of shared libs claude needs.- Linux-specific virtual filesystems —
linprocfs,linsysfs,fdescfs,tmpfsfor/dev/shm. Mounted automatically when you start the linux service.
Verify all three are up:
$ kldstat | grep linux64
12 1 0xffffffff84ad0000 7e8b8 linux64.ko
$ ls /compat/linux | head
bin dev etc lib lib64 proc sys usr var
$ mount | grep -i linux
linprocfs on /compat/linux/proc (linprocfs, ...)
linsysfs on /compat/linux/sys (linsysfs, ...)
fdescfs on /compat/linux/dev/fd (fdescfs, linrdlnk, ...) # <-- linrdlnk MATTERS
tmpfs on /compat/linux/dev/shm (tmpfs, ...)
The one trap: fdescfs needs linrdlnk
On FreeBSD’s host, /etc/rc.d/linux mounts fdescfs at /compat/linux/dev/fd with the linrdlnk option. This is not optional for bun-based apps like Claude.
Bun resolves the current working directory by open(path, O_PATH) followed by readlink("/dev/fd/<N>", …). The default fdescfs returns character-device entries that don’t readlink() — so readlink returns EINVAL, claude can’t create its lock file, and claude silently deadlocks at startup with no output, no error, nothing. SIGINT works; the truss trail dead-ends in linux_epoll_pwait2.
If you ever see claude hang at startup with a blank screen on FreeBSD:
$ ls -la /compat/linux/dev/fd/0
lr-xr-xr-x ... 0 -> /dev/null # GOOD — symlinks
crw-rw-rw- ... /dev/fd/0 # BAD — character device, no linrdlnk
If it’s a character device, remount:
doas umount /compat/linux/dev/fd
doas mount -t fdescfs -o linrdlnk fdesc /compat/linux/dev/fd
The default service linux start does the right thing — this only breaks if you hand-rolled the mount or copied an old jail fstab.
Installing Claude
Anthropic’s installer is a non-interactive bash script that downloads the binary into ~/.local/share/claude/versions/<v> and symlinks ~/.local/bin/claude:
curl -fsSL https://claude.ai/install.sh | bash
Make sure ~/.local/bin is on your PATH:
echo 'export PATH=$HOME/.local/bin:$PATH' >> ~/.zshrc
Running it
claude --dangerously-skip-permissions
The first run prompts for browser-based auth and writes credentials to ~/.claude.json. From then on you’re good.
When to reach for a jail instead
Running directly on the host is fine for solo use. Reach for a jail (see the ephemeral-claude-jails post) if you want any of:
- Multiple parallel claude instances with separate
~/.claude.jsonstate. - Filesystem isolation — claude can read everything in your home directory by default.
- A throwaway environment you can
zfs rollbackafter experiments. - Different OS-level packages installed in the claude environment vs your host.
Updating Claude
The binary self-updates:
claude update
It writes a new version into ~/.local/share/claude/versions/ and swings the symlink. No reinstall needed.
Troubleshooting checklist
| symptom | fix |
|---|---|
Exec format error | linux64.ko not loaded; run doas service linux start |
No such file: /lib64/ld-linux-x86-64.so.2 | linux_base-rl9 not installed |
| Hangs at startup, no output | fdescfs missing linrdlnk (see above) |
permission denied writing ~/.local/state/claude/locks | dir doesn’t exist; mkdir -p ~/.local/state/claude/locks |
EAGAIN opening /proc/self/... | linprocfs not mounted on /compat/linux/proc |
See also
- Ephemeral Claude jails on FreeBSD — sandboxed, parallel, cloneable.
- OpenCL with AMD Radeon on FreeBSD — for using the GPU from hashcat or other OpenCL workloads claude might launch.