FreeBSD
ZeroClaw runs natively on FreeBSD (tested on FreeBSD 15.0-RELEASE, amd64). Two things differ from the Linux/macOS/Windows paths:
- No prebuilt binary and no
install.shsupport. FreeBSD is not a target of the bootstrap installer, so you build from source with the system Rust toolchain. - No
zeroclaw servicebackend. Thezeroclaw service installcommand knows systemd, OpenRC, launchd, and Windows Task Scheduler, not FreeBSDrc.d. You install a smallrc.dscript yourself. This page gives you a complete, tested one.
Everything else, config, providers, channels, the daemon, the gateway, is identical to any other platform.
When to use FreeBSD. FreeBSD deployments are common in network-appliance, embedded, and jail-based hosting where operators want the base system’s stability, ZFS + jail primitives, or need FreeBSD for policy/licensing reasons. Because there is no prebuilt binary and the
rc.dsetup is manual, this path suits operators comfortable with FreeBSD conventions. If you are only evaluating platforms with no specific FreeBSD requirement, Linux (systemd) or macOS (launchd) onboard faster viainstall.shandzeroclaw service install.Grab the files instead of copy-pasting. Every shell script and sample config shown below ships in
dist/freebsd/: copy them to your host directly. The walkthrough explains what each piece does and why.
System dependencies
Install the toolchain and runtime from pkg:
sh
doas pkg install -y rust git
| Package | Why |
|---|---|
rust | Provides cargo and rustc to build the binary. ZeroClaw’s workspace MSRV is Rust 1.87; the FreeBSD rust port tracks a newer stable, so pkg install rust satisfies it. |
git | Cloning the repo, and required at runtime if you use any git-backed tools. |
doas, notsudo. FreeBSD shipsdoasas the base privilege-escalation tool;sudois an optional port. The examples here usedoas. A minimal/usr/local/etc/doas.confgranting thewheelgroup passwordless escalation is:permit nopass keepenv :wheel
Build from source
sh
git clone https://github.com/zeroclaw-labs/zeroclaw.git
cd zeroclaw
cargo build --release
The release binary lands at target/release/zeroclaw. A clean build of the default feature set takes a while on modest hardware, this is expected; ZeroClaw is a large Rust workspace.
To trim the build, disable features you don’t need (see ./install.sh --list-features on a Linux box, or Cargo.toml):
sh
cargo build --release --no-default-features --features agent-runtime
Install the binary
Put it somewhere on PATH. /usr/local/bin is the conventional location for ports-installed binaries on FreeBSD:
sh
doas install -m 755 target/release/zeroclaw /usr/local/bin/zeroclaw
zeroclaw --version
(~/.cargo/bin/zeroclaw works just as well if you’d rather keep it per-user.)
First-run configuration
sh
zeroclaw quickstart
This creates ~/.zeroclaw/ with a starter config and walks you through provider setup. Config layout and precedence are identical to every other platform: see Reference → Config.
Provider authentication
Provider auth is not FreeBSD-specific. API-key providers just need the key set through the gateway, zerocode, zeroclaw config set, or the environment. OAuth and subscription providers (e.g. an OpenAI/Codex ChatGPT subscription, Anthropic Claude Pro/Team) get their token from the vendor’s own dashboard or login flow, which you then configure the same way you would an API key.
For the full credential model (API keys, OAuth/subscription tokens, env overrides, and the secrets store), see Provider Configuration → Credentials and OAuth and subscription auth. That page is the source of truth for every platform.
Running as a service (rc.d)
Because zeroclaw service install has no FreeBSD backend, supervise the daemon with FreeBSD’s native daemon(8) under an rc.d script. This gives you service zeroclaw start|stop|restart|status, restart-on-crash, a pidfile, and boot-time startup.
Ready-to-install copies of every script below live in
dist/freebsd/(zeroclaw-run.sh, the basiczeroclaw.rc, and the hardenedzeroclaw-hardened.rc). The tworc.dscripts carry a@@ZEROCLAW_USER@@placeholder yousedin on install, so you can grab the files instead of copy-pasting: seedist/freebsd/README.md. The walkthrough below explains what each piece does.
1. Launcher script
daemon(8) starts the child with a minimal environment, so export a full PATH (FreeBSD puts git, python3, etc. under /usr/local/bin, which is not on the default service PATH). The rc.d script runs this through daemon -u <user>, which per daemon(8) sets HOME, USER, and SHELL from that account’s passwd entry before exec, so ${HOME} is already the service account’s home (accounts whose home is elsewhere, and rc.conf run-as overrides, just work). Save as /usr/local/libexec/zeroclaw-run.sh:
sh
#!/bin/sh
# daemon -u <user> has already set HOME from the account's passwd entry.
export PATH="/usr/local/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin:${HOME}/bin"
exec /usr/local/bin/zeroclaw daemon --config-dir "${HOME}/.zeroclaw"
sh
doas install -m 755 zeroclaw-run.sh /usr/local/libexec/zeroclaw-run.sh
2. rc.d script
Save as /usr/local/etc/rc.d/zeroclaw:
sh
#!/bin/sh
#
# PROVIDE: zeroclaw
# REQUIRE: NETWORKING DAEMON
# KEYWORD: shutdown
. /etc/rc.subr
name="zeroclaw"
rcvar="zeroclaw_enable"
load_rc_config $name
: ${zeroclaw_enable:="NO"}
# Do NOT name this ${name}_user — rc.subr would then run its own su user-switch
# and collide with daemon -u ("failed to set user environment").
: ${zeroclaw_runas:="youruser"}
rundir="/var/run/zeroclaw"
pidfile="${rundir}/zeroclaw.pid"
logfile="/var/log/${name}.log"
launcher="/usr/local/libexec/zeroclaw-run.sh"
command="/usr/sbin/daemon"
command_args="-r -P ${pidfile} -o ${logfile} -u ${zeroclaw_runas} ${launcher}"
start_precmd="zeroclaw_precmd"
zeroclaw_precmd()
{
# rundir + logfile stay root-owned: rc.d (root) writes the daemon -P pidfile
# here and trusts it later, so the unprivileged service user must not be able
# to forge it. daemon -o opens the logfile before dropping to ${zeroclaw_runas}.
install -d -o root -g wheel -m 755 "${rundir}"
install -o root -g wheel -m 640 /dev/null "${logfile}"
}
run_rc_command "$1"
sh
doas install -m 755 zeroclaw /usr/local/etc/rc.d/zeroclaw
What the flags do:
-r: supervise and restart the child if it exits (crash recovery).-P ${pidfile}: write the supervisor’s pid soservice zeroclaw stopcan signal it.-o ${logfile}: redirect the child’s stdout/stderr to a logfile.-u ${zeroclaw_runas}: run zeroclaw as an unprivileged user, not root.
Why
daemon -uand notsu -m. A common pattern isdaemon ... su -m user -c launcher. Avoid it:su(1)does not forwardSIGTERMto its child, soservice zeroclaw stopkills thedaemonsupervisor but leaves an orphanedzeroclawprocess behind, and the nextstartstacks a second copy.daemon -u usermakesdaemon(8)the direct parent ofzeroclaw, so it forwards the stop signal and shuts down cleanly. (If you’re stuck with asu-based script for other reasons, add apkill -f "zeroclaw daemon"sweep to its stop path.)
3. Enable and start
sh
doas sysrc zeroclaw_enable=YES
doas sysrc zeroclaw_runas=youruser # the account that owns ~/.zeroclaw
doas service zeroclaw start
doas service zeroclaw status
service zeroclaw stop / restart work as expected. Because zeroclaw_enable=YES is in /etc/rc.conf (written by sysrc), the daemon also starts on boot.
4. Hardening for unattended and remote operation
The script above is correct for an interactive, single-instance install. Three daemon(8) behaviours will surprise you the moment you drive the service remotely (over ssh) or run more than one copy. All three bit a production deployment; the fixes are small. A complete script folding in every fix below ships as dist/freebsd/zeroclaw-hardened.rc: install it in place of the basic zeroclaw script.
Remote service ... start hangs. daemon -r inherits and holds open whatever stdin/stdout/stderr it was launched with. Run ssh host 'service zeroclaw start' and the supervisor keeps your ssh session’s stdout fd open forever, so ssh never sees EOF and the command hangs even though the daemon started fine. Detach the supervisor’s own descriptors: -o ${logfile} already routes the child’s output, so nothing is lost:
sh
command_args="-r -P ${pidfile} -o ${logfile} -u ${zeroclaw_runas} ${launcher}"
# ...invoke daemon with its own std{in,out,err} sent to /dev/null:
/usr/sbin/daemon ${command_args} </dev/null >/dev/null 2>&1
If you use the stock command/command_args form, wrap the start in a custom start_cmd so you control the redirection. This one change is what makes service zeroclaw start safe to call from ssh, CI, or a config-management push.
Repeated start stacks orphan supervisors. A plain start does not check whether a supervisor is already running, so a second start (or a start after a crash that left a stale pidfile) launches another daemon that fights the first over the gateway port. Make start idempotent by refusing when a live supervisor already exists. Match the supervisor by the launcher path, not the pidfile alone (the pidfile can be stale). Two FreeBSD-specific traps when you do this:
daemon(8)retitles its supervisor todaemon: /usr/local/libexec/zeroclaw-run.sh[<childpid>] (daemon). Sopgrep -f zeroclaw-run.shmatches the supervisor, but apgrep -ffor the binary name does not. Bind on the literaldaemon:prefix: that matches the supervisor and never the child, a hand-run of the launcher, or the rc shell itself. Bind the trailing[that opens daemon’s[<childpid>]too, so a sibling launcher whose name merely starts withzeroclaw-run.shcan’t match (this matters once you run a pool, see Running a pool of instances below).- FreeBSD
pgrep -fdoes not honour a leading^anchor against that retitle string:pgrep -f '^daemon: ...'matches nothing. Drop the^; rely on thedaemon:prefix for specificity and escape the dot in.shas[.](and the bracket as[[]) so they are literal.
sh
launcher_pat="daemon: /usr/local/libexec/zeroclaw-run[.]sh[[]"
zeroclaw_running()
{
pgrep -f "${launcher_pat}" >/dev/null 2>&1
}
read from a daemon -P pidfile reports a false negative. daemon -P writes the pid with no trailing newline, so IFS= read -r pid < "${pidfile}" returns a non-zero status (EOF before newline) even though it set pid correctly. If you guard it as read -r pid < "$pf" || return 1, every running instance looks stopped and your idempotent start happily launches a duplicate. Don’t key the success path on read’s exit status: validate the value instead:
sh
pid=""
IFS= read -r pid < "${pidfile}" # do NOT `|| return 1` here
case "${pid}" in
''|*[!0-9]*) return 1 ;; # empty or non-numeric → treat as not running
esac
Running a pool of instances. To run N daemons (e.g. a worker pool), give each its own pidfile and logfile (worker.$i.pid, worker.$i.log) and loop the start/stop over $i. Because the supervisor retitle is identical for every instance and does not include per-instance arguments, the pidfile is the only per-instance handle: drive stop/status from the pidfile, and on a full stop sweep any leftover supervisor that no live pidfile points at (started by hand, or whose pidfile went stale).
Running in a jail
Jails give ZeroClaw an isolated root with its own packages, service user, and optionally its own IP, useful if the host runs other services or you want to constrain the agent. The service setup is identical to the host case; you just run it inside the jail. This walks through a classic thick jail with base-system tooling (no jail manager required).
One-step option.
dist/freebsd/zeroclaw-jail-setup.shautomates steps 1–3 below: it creates the jail, extracts a matching base, adds the/etc/jail.confentry, starts the jail, and installs the launcher + hardenedrc.dscript inside it (doas sh zeroclaw-jail-setup.sh, withJAIL_NAME/JAIL_PATH/ZPOOL/ZEROCLAW_USERoverridable via env). The manual walkthrough below explains what it does.
1. Create the jail
sh
# ZFS dataset for the jail (use a plain directory if you're on UFS).
doas zfs create -o mountpoint=/jails/zeroclaw zroot/jails/zeroclaw # adjust pool
# Extract a base matching the HOST's release into it.
doas fetch -o /tmp/base.txz \
"https://download.freebsd.org/releases/$(uname -m)/$(freebsd-version -u)/base.txz"
doas tar -xpf /tmp/base.txz -C /jails/zeroclaw
doas cp /etc/resolv.conf /jails/zeroclaw/etc/
2. Configure and start
Add a jail entry to /etc/jail.conf (host side). This example shares the host network; set ip4.addr instead if you give the jail a dedicated address.
zeroclaw {
host.hostname = "zeroclaw";
path = "/jails/zeroclaw";
exec.start = "/bin/sh /etc/rc";
exec.stop = "/bin/sh /etc/rc.shutdown";
exec.clean;
mount.devfs;
persist;
}
sh
doas sysrc jail_enable=YES
doas sysrc jail_list+=" zeroclaw"
doas service jail start zeroclaw
3. Install ZeroClaw inside the jail
Everything from the sections above runs inside the jail: prefix commands with doas jexec zeroclaw …, or open a shell with doas jexec zeroclaw /bin/sh:
sh
doas jexec zeroclaw pkg install -y rust git # or copy a binary built on the host
# build + install zeroclaw to /usr/local/bin/zeroclaw exactly as above, then:
doas jexec zeroclaw pw useradd zeroclaw -m -s /usr/sbin/nologin
Install the launcher and rc.d script into the jail’s filesystem (from the host, the jail root is prefixed: /jails/zeroclaw/usr/local/libexec/… and /jails/zeroclaw/usr/local/etc/rc.d/…). Then enable and start the service inside the jail:
sh
doas jexec zeroclaw sysrc zeroclaw_enable=YES
doas jexec zeroclaw service zeroclaw start
doas jexec zeroclaw service zeroclaw status
Jail-specific notes
- Edit jail files from the host with
tee, notcp /dev/stdin. Pipe through… | doas tee /jails/zeroclaw/usr/local/etc/rc.d/zeroclaw >/dev/null;doas cp /dev/stdin …can fail mid-copy withcp: /dev/stdin: File changed. - The gateway binds inside the jail. The daemon listens on loopback by default: to reach it from the host or LAN, launch zeroclaw with
--host 0.0.0.0(editzeroclaw-run.sh) and give the jail a reachable address, or proxy from the host. - Prefer the hardened
rc.dscript in a jail. You’ll typically driveservicenon-interactively viajexec/ssh, which is exactly where the basic script’sstarthang and orphan-stacking bite: see Hardening. It also keeps/var/run/zeroclawroot-owned inside the jail so the unprivileged service user can’t forge the supervisor pidfile. - Running several daemons in one jail (e.g. a worker pool) follows the pool note in the hardening section: one pidfile/logfile per instance and a
pgrepbound to the launcher retitle, since the jail shares one process table.
Running the Linux image under Podman + Linuxulator
The native build above is the right path for ZeroClaw itself. But some Python-backed
tools and skills depend on manylinux-only wheels: polars, pyarrow, and
oracledb, for example, publish no FreeBSD wheels, so a tool that imports them can’t
run under the native FreeBSD python3. FreeBSD’s Linuxulator
(Linux binary-compatibility layer) plus Podman lets you run the official Linux
container image on a FreeBSD host, giving those tools the Linux ABI they expect.
This complements the native rc.d daemon: you can run either, or both side by side.
1. Prerequisites
Enable the Linux ABI and confirm it reports a Linux release:
sh
doas sysrc linux_enable="YES"
doas service linux start # loads the modules and mounts /compat/linux
sysctl compat.linux.osrelease # e.g. compat.linux.osrelease: 5.15.0
linux_enable="YES" in /etc/rc.conf also loads the ABI on boot. Then install Podman:
sh
doas pkg install -y podman
2. Pull the image: force the Linux platform
FreeBSD Podman defaults to os=freebsd when resolving a manifest list. ZeroClaw’s
images are published only for linux/amd64 and linux/arm64, so a plain podman pull
fails with no image found in manifest list for architecture ..., OS freebsd. Force
the Linux platform explicitly:
sh
doas podman pull --os linux --arch amd64 ghcr.io/zeroclaw-labs/zeroclaw:debian
Use the
debiantag rather thanlatest: the distrolesslatestimage has no shell, which makes it awkward to debug under emulation. See Docker & Containers for the full image list.
3. Run the container
The Linux image behaves exactly as documented in Docker & Containers,
it expects persistent state at /zeroclaw-data and bootstraps a config on first run:
sh
doas podman run -d --name zeroclaw --restart=always \
--os linux --arch amd64 \
-p 42617:42617 \
-v /var/db/zeroclaw:/zeroclaw-data \
ghcr.io/zeroclaw-labs/zeroclaw:debian
doas podman exec -it zeroclaw zeroclaw quickstart
Keep the --os linux --arch amd64 flags on every run (not just pull) so Podman
doesn’t re-resolve to the FreeBSD default.
Linuxulator notes
- Boot persistence. Podman’s own
--restart=alwaysonly restarts the container within a running Podman; it won’t survive a host reboot on its own. Supervise thepodman startfrom anrc.dscript (same pattern as the native service) or a@rebootcron entry so the container comes back after the host restarts. - Networking.
-p 42617:42617publishes the gateway through Podman’s bridge. If the bridge/CNI setup isn’t configured on your host,--network hostis the simplest alternative: the container then shares the host’s network stack directly. - Not everything emulates cleanly. Linuxulator covers the common syscall surface,
but exotic binaries may hit unimplemented calls. If a tool misbehaves, check
dmesgforlinux:warnings before assuming a ZeroClaw bug.
Logs
sh
tail -f /var/log/zeroclaw.log
Set the log level via the standard config / env knobs: see Operations → Logs & observability.
Verify
sh
zeroclaw --version
service zeroclaw status
# if the daemon exposes the local gateway (default 127.0.0.1:42617):
fetch -qo - http://127.0.0.1:42617/health
A "status":"ok" health payload means the gateway is up; the response’s runtime field carries the per-component health (channels, providers, and so on).
Uninstall
sh
doas service zeroclaw stop
doas sysrc -x zeroclaw_enable
doas rm /usr/local/etc/rc.d/zeroclaw /usr/local/libexec/zeroclaw-run.sh
doas rm /usr/local/bin/zeroclaw
rm -rf ~/.zeroclaw # optional — deletes config + history
Next
- Service management: how the first-party backends work on other platforms
- Reference → Config: config file layout
- Quickstart: first conversation
- Operations → Overview: running in production