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:

  1. The linux64 kernel module — a Linux ABI shim that translates Linux syscalls into FreeBSD ones. Loaded by service linux start.
  2. linux_base-rl9 — a Rocky Linux 9 userland mounted under /compat/linux. Provides ld-linux-x86-64.so.2, glibc, and the handful of shared libs claude needs.
  3. Linux-specific virtual filesystemslinprocfs, linsysfs, fdescfs, tmpfs for /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.json state.
  • Filesystem isolation — claude can read everything in your home directory by default.
  • A throwaway environment you can zfs rollback after 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

symptomfix
Exec format errorlinux64.ko not loaded; run doas service linux start
No such file: /lib64/ld-linux-x86-64.so.2linux_base-rl9 not installed
Hangs at startup, no outputfdescfs missing linrdlnk (see above)
permission denied writing ~/.local/state/claude/locksdir doesn’t exist; mkdir -p ~/.local/state/claude/locks
EAGAIN opening /proc/self/...linprocfs not mounted on /compat/linux/proc

See also