Exploring alternatives to OpenClaw โ can Clawdie run on FreeBSD with bhyve, or even better, with native jails?
Clawdie uses Docker containers to isolate agent execution. Each agent runs in a Linux container with:
@anthropic-ai/claude-code CLIFreeBSD doesn't have native Docker. But we have bhyve โ the BSD hypervisor. Can we make it work?
Run the entire Clawdie stack inside a Debian/Ubuntu bhyve VM.
| Pros | Cons |
|---|---|
| Zero code changes | Resource overhead (separate kernel, init, etc.) |
| All features work out of box | Managing two OS environments |
| Official Docker support | More complexity for updates/backups |
Node.js runs natively on FreeBSD. Only Docker runs in a minimal Linux VM.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ FreeBSD 15 Host โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Node.js (Native) โ โ
โ โ โโโ Clawdie main process โ โ
โ โ โโโ Telegram/WhatsApp channel โ โ
โ โ โโโ Message routing & IPC โ โ
โ โ โโโ SQLite database โ โ
โ โ โโโ tmux glass-pane โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โ DOCKER_HOST=tcp://10.0.0.2:2375 โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ bhyve Linux VM (Alpine or Debian minimal) โ โ
โ โ โโโ Docker daemon only โ โ
โ โ โโโ nanoclaw-agent images โ โ
โ โ โโโ ~512MB RAM, 1 vCPU โ โ
โ โ โโโ No SSH, no GUI, just Docker API โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
| Pros | Cons |
|---|---|
| FreeBSD stability, ZFS, jails ecosystem | Need to maintain tiny Docker VM |
| Zero code changes to Clawdie | Network bridge configuration |
| Lightweight (VM is ~200MB disk) | TLS for Docker API (security) |
| Host benefits: pf, audit, snapshots | Slight latency to Docker API |
Create a FreeBSD-specific container runtime abstraction (similar to the Apple Container skill).
| Pros | Cons |
|---|---|
| Native feel, no VM | Significant development effort |
| Clean abstraction layer | Linux containers won't work anyway |
| Would need FreeBSD jails + Linux compat |
Run the agent directly on the FreeBSD host.
Agents would have full system access. No isolation. One bad prompt could delete everything. Only viable for fully trusted environments.
Use FreeBSD jails with nullfs mounts to isolate agents โ no Docker, no Linux VM, pure FreeBSD.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ FreeBSD 15 Host โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Jail: nanoclaw โ โ
โ โ โ โ
โ โ /home/nanoclaw/ โ nullfs RO (host:/home/nanoclaw) โ โ
โ โ /groups/ โ nullfs RW (host:/path/groups) โ โ
โ โ /data/ โ nullfs RW (host:/path/data) โ โ
โ โ /workspace/ipc/ โ nullfs RW (host:/path/ipc) โ โ
โ โ โ โ
โ โ โโโ Node.js 22 (installed in jail) โ โ
โ โ โโโ Chromium (installed in jail) โ โ
โ โ โโโ claude-code CLI (installed in jail) โ โ
โ โ โโโ Agent runs here, can ONLY see jail filesystem โ โ
โ โ โ โ
โ โ Even "rm -rf /" only destroys the jail, not host โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Host is protected โ jail has no access to /etc, /root, etc. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
| Pros | Cons |
|---|---|
| Native FreeBSD (no Linux emulation) | Small code changes needed (~5 path env vars) |
| Real isolation (jail can't escape) | Need to install Chromium in jail |
| Lower overhead than bhyve VM | bastille or ezjail setup required |
| ZFS snapshots of jail dataset | No Linux container compatibility |
| Same kernel = faster agent spawn | Agent-runner paths need configuration |
The agent-runner has only 5 hardcoded paths
(/workspace/group, /workspace/ipc, etc.).
Making these configurable via environment variables enables jail
execution with zero architectural changes.
Two viable approaches emerged from this exploration:
The container-runtime.ts file in Clawdie is already an
abstraction layer โ only 87 lines, designed to be swappable. The only
change needed:
# In .env on FreeBSD host:
DOCKER_HOST=tcp://10.0.0.2:2375
That's it. The Node.js process thinks it's talking to local Docker, but it's actually talking to Docker inside the VM.
For a small code change (5 environment variables), you get native FreeBSD performance with real isolation. The agent-runner's hardcoded paths become configurable:
# Environment variables for jail mode:
NANOCLAW_IPC_DIR=/workspace/ipc/input
NANOCLAW_WORKSPACE=/groups/main
NANOCLAW_CONVERSATIONS_DIR=/groups/main/conversations
NANOCLAW_GLOBAL_MD=/workspace/global/CLAUDE.md
NANOCLAW_EXTRA_DIR=/workspace/extra
Clawdie's architecture already separates concerns:
| Component | Runs Where | Why |
|---|---|---|
| Channel (Telegram/WhatsApp) | FreeBSD host | Native Node.js, no Linux deps |
| Message routing | FreeBSD host | Pure JavaScript |
| SQLite database | FreeBSD host | Native better-sqlite3 works |
| Agent execution | Docker VM or Jail | Needs isolation + Chromium |
# Install vm-bhyve (VM manager)
pkg install vm-bhyve
# Create ZFS dataset for VMs
zfs create -o mountpoint=/vms zroot/vms
# Initialize vm-bhyve
sysrc vm_enable="YES"
sysrc vm_dir="zfs:zroot/vms"
vm init
# Download Alpine Linux (minimal, ~50MB)
vm iso https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/x86_64/alpine-virt-3.20.0-x86_64.iso
# Create VM (512MB RAM, 1 vCPU, 4GB disk)
vm create -t alpine -s 4G -m 512M -c 1 docker-vm
# Install Alpine
vm install docker-vm alpine-virt-3.20.0-x86_64.iso
# Inside the VM console:
vm console docker-vm
# Setup Alpine
setup-alpine # Choose "sys" mode for disk install
# After reboot, install Docker
apk add docker
rc-update add docker boot
service docker start
# Enable TCP API (edit /etc/init.d/docker or use override)
# Create /etc/docker/daemon.json:
{
"hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2375"]
}
# Restart Docker
service docker restart
# On FreeBSD host, create bridge for VM communication
# Add to /etc/rc.conf:
cloned_interfaces="bridge0"
ifconfig_bridge0="addm em0" # Replace em0 with your NIC
# Configure VM with static IP (edit /vms/docker-vm/vm.conf)
# Add network config to get 10.0.0.2 or use DHCP
# Install Node.js 22
pkg install node22 npm
# Clone Clawdie
git clone https://codeberg.org/Clawdie/nanoclaw.git /home/nanoclaw
cd /home/nanoclaw
# Install dependencies
npm install
# Build
npm run build
# Configure Docker host
echo "DOCKER_HOST=tcp://10.0.0.2:2375" >> .env
# On FreeBSD host, tell Docker (in VM) to build the image
DOCKER_HOST=tcp://10.0.0.2:2375 docker build -t nanoclaw-agent:latest container/
# Verify
DOCKER_HOST=tcp://10.0.0.2:2375 docker images
Use the /add-telegram skill to add Telegram support
(replaces WhatsApp):
# Create bot with @BotFather, get token
# Add to .env:
TELEGRAM_BOT_TOKEN=123456:ABC-DEF...
TELEGRAM_ONLY=true
This approach uses FreeBSD's native jail system instead of Docker. The agent runs isolated but shares the host kernel โ no VM overhead.
The agent-runner has 5 hardcoded paths. Make them configurable:
// In container/agent-runner/src/index.ts, change:
// Before (hardcoded):
const IPC_INPUT_DIR = '/workspace/ipc/input';
const conversationsDir = '/workspace/group/conversations';
const globalClaudeMdPath = '/workspace/global/CLAUDE.md';
const extraBase = '/workspace/extra';
cwd: '/workspace/group'
// After (configurable):
const IPC_INPUT_DIR = process.env.NANOCLAW_IPC_DIR || '/workspace/ipc/input';
const conversationsDir = process.env.NANOCLAW_CONVERSATIONS_DIR || '/workspace/group/conversations';
const globalClaudeMdPath = process.env.NANOCLAW_GLOBAL_MD || '/workspace/global/CLAUDE.md';
const extraBase = process.env.NANOCLAW_EXTRA_DIR || '/workspace/extra';
// cwd becomes: process.env.NANOCLAW_WORKSPACE || '/workspace/group'
# bastille is the modern FreeBSD jail manager
pkg install bastille
# Enable it
sysrc bastille_enable="YES"
# Initialize with ZFS
bastille bootstrap zfs
# Create a thin jail (uses ZFS snapshots, minimal disk)
bastille create nanoclaw 15.0-RELEASE 10.0.0.10
# Or use a base template for faster creation:
bastille create -T nanoclaw 15.0-RELEASE 10.0.0.10
# Enter the jail
bastille console nanoclaw
# Inside jail:
pkg install node22 npm chromium
# Install claude-code globally
npm install -g @anthropic-ai/claude-code
# Verify Chromium works
chromium --version
# On HOST, edit /etc/jail.conf.d/nanoclaw.conf or bastille config:
# Mount Clawdie code (read-only for safety)
# Create mount points in jail first:
bastille exec nanoclaw mkdir -p /home/nanoclaw /groups /data /workspace/ipc
# Add to /etc/fstab (host):
/home/nanoclaw /jails/nanoclaw/root/home/nanoclaw nullfs ro 0 0
/home/nanoclaw/groups/main /jails/nanoclaw/root/groups/main nullfs rw 0 0
/home/nanoclaw/data /jails/nanoclaw/root/data nullfs rw 0 0
/home/nanoclaw/data/ipc /jails/nanoclaw/root/workspace/ipc nullfs rw 0 0
# Or use bastille's mount command:
bastille mount nanoclaw /home/nanoclaw /home/nanoclaw nullfs ro
bastille mount nanoclaw /home/nanoclaw/groups/main /groups/main nullfs rw
bastille mount nanoclaw /home/nanoclaw/data /data nullfs rw
Replace Docker container spawn with jail exec:
#!/bin/sh
# run-agent-in-jail.sh
JAIL_NAME="nanoclaw"
GROUP_FOLDER="$1"
PROMPT="$2"
# Environment for agent-runner
export NANOCLAW_IPC_DIR="/workspace/ipc/input"
export NANOCLAW_CONVERSATIONS_DIR="/groups/${GROUP_FOLDER}/conversations"
export NANOCLAW_WORKSPACE="/groups/${GROUP_FOLDER}"
export NANOCLAW_GLOBAL_MD="/workspace/global/CLAUDE.md"
export NANOCLAW_EXTRA_DIR="/workspace/extra"
# Run agent inside jail
echo "{\"prompt\":\"${PROMPT}\",\"groupFolder\":\"${GROUP_FOLDER}\"}" | \
bastille exec $JAIL_NAME node /home/nanoclaw/container/agent-runner/dist/index.js
// In src/container-runner.ts, add jail mode:
const USE_JAIL = process.env.NANOCLAW_USE_JAIL === 'true';
if (USE_JAIL) {
// Spawn process in jail instead of Docker container
const jail = spawn('bastille', [
'exec', 'nanoclaw',
'node', '/home/nanoclaw/container/agent-runner/dist/index.js'
], { stdio: ['pipe', 'pipe', 'pipe'] });
// Rest is same โ stdin JSON, stdout parsing, etc.
} else {
// Existing Docker code...
}
# In jail.conf, add restrictions:
nanoclaw {
host.hostname = nanoclaw;
ip4.addr = 10.0.0.10;
path = /jails/nanoclaw/root;
# Security settings
securelevel = 2;
enforce_statfs = 2;
children.max = 0;
allow.raw_sockets = 0;
allow.sysvipc = 0;
allow.mount = 0;
allow.mount.devfs = 0;
allow.quotas = 0;
# Network restrictions (optional)
interface = lo1;
}
# Create loopback interface for jail isolation:
cloned_interfaces="lo1"
ifconfig_lo1_name="jail0"
| Aspect | Docker (Option B) | Jail (Option D+) |
|---|---|---|
| Isolation level | Container (namespaces) | Jail (chroot + syscall filter) |
| Kernel | Linux (in VM) | FreeBSD (shared) |
| Memory overhead | ~512MB (VM) + container | ~0 (shared kernel) |
| Startup time | ~2-5s (VM always on) | ~0.1s (process spawn) |
| Code changes | None | ~5 env vars + spawn logic |
| Chromium | In container | In jail |
| ZFS snapshots | VM disk image | Jail dataset directly |
The tmux glass-pane concept from Clawdie adapts perfectly to Clawdie. Instead of watching an OpenClaw gateway, you watch Clawdie's main process + container logs.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ tmux session: nanoclaw โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ โ
โ Clawdie main process โ Interactive shell โ
โ (npm start / dev) โ (for debugging, tests) โ
โ โ โ
โ [agent spawning logs] โ $ sqlite3 store/messages.db โ
โ [message routing] โ $ tail -f logs/nanoclaw.log โ
โ [IPC activity] โ โ
โ โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ btop / htop (system monitoring) โ
โ - Container CPU/RAM usage โ
โ - FreeBSD host resources โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
start-nanoclaw.sh#!/bin/sh
# start-nanoclaw.sh โ tmux glass-pane launcher for FreeBSD
SESSION="nanoclaw"
PROJECT="/home/nanoclaw"
# Kill existing session if any
tmux kill-session -t $SESSION 2>/dev/null
# Create new session with main process
tmux new-session -d -s $SESSION -c $PROJECT
# Split horizontally (shell on right)
tmux split-window -h -t $SESSION
# Split bottom (monitor)
tmux split-window -v -t $SESSION:0.0
# Pane 0: Clawdie main process
tmux send-keys -t $SESSION:0.0 'cd /home/nanoclaw && DOCKER_HOST=tcp://10.0.0.2:2375 npm start' Enter
# Pane 1: Shell
tmux send-keys -t $SESSION:0.1 'cd /home/nanoclaw' Enter
# Pane 2: btop (or htop)
tmux send-keys -t $SESSION:0.2 'btop' Enter
# Set layout
tmux select-layout -t $SESSION main-horizontal
# Attach
tmux attach -t $SESSION
| From | Command |
|---|---|
| Local (FreeBSD console) | tmux attach -t nanoclaw |
| SSH from laptop |
ssh user@freebsd-host -t "tmux attach -t nanoclaw"
|
| Read-only monitor | tmux attach -t nanoclaw -r |
| Check Docker VM | vm console docker-vm |
This instance comes with Stripe Agents toolkit preconfigured for payment processing. The agent can handle customer lookups, payment links, subscriptions, and billing operations.
| Path | Use Case | Status |
|---|---|---|
| MCP Server | Claude Desktop, Cursor, existing MCP setups | โ Ready |
| Python Toolkit | Custom agents, LangChain, CrewAI | โ Ready |
| TypeScript Toolkit | Node.js apps, Vercel AI SDK | โ Ready |
# In .env (NOT committed to repo):
STRIPE_SECRET_KEY=rk_test_... # Restricted API Key (test mode)
STRIPE_WEBHOOK_SECRET=whsec_... # For webhook verification
# MCP config (for Claude/Cursor integration):
# See .claude/mcp-servers.json.example
.env or real keys| Category | Operations |
|---|---|
| Payments | Create/retrieve charges, Payment Links, refunds |
| Customers | Create/list/update customers, payment methods |
| Products | CRUD products and prices |
| Billing | Subscriptions, invoices, credit notes |
| Balance | Retrieve balance, transactions |
# Via Telegram to Clawdie:
"Look up customer john@example.com in Stripe"
# Agent will:
# 1. Search Stripe customers by email
# 2. Return payment history, subscription status
# 3. Offer actions (update, refund, etc.)
Exposing Docker API on TCP without TLS allows anyone on the network to run containers as root. Use TLS or restrict to internal network only.
# On Docker VM, generate certs
apk add openssl
mkdir -p /etc/docker/certs
cd /etc/docker/certs
# Generate CA
openssl genrsa -out ca-key.pem 4096
openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem
# Generate server cert
openssl genrsa -out server-key.pem 4096
openssl req -new -key server-key.pem -out server.csr
openssl x509 -req -days 365 -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem
# Generate client cert (for FreeBSD host)
openssl genrsa -out key.pem 4096
openssl req -new -key key.pem -out client.csr
openssl x509 -req -days 365 -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out cert.pem
# Update /etc/docker/daemon.json:
{
"hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"],
"tls": true,
"tlscacert": "/etc/docker/certs/ca.pem",
"tlscert": "/etc/docker/certs/server-cert.pem",
"tlskey": "/etc/docker/certs/server-key.pem"
}
# Copy ca.pem, cert.pem, key.pem to FreeBSD host
# Set DOCKER_HOST and DOCKER_TLS_VERIFY:
export DOCKER_HOST=tcp://10.0.0.2:2376
export DOCKER_TLS_VERIFY=1
export DOCKER_CERT_PATH=/home/nanoclaw/docker-certs
# /etc/pf.conf
# Block Docker API from external access
block in on egress proto tcp from any to any port 2375:2376
# Allow only from localhost and internal bridge
pass in on bridge0 proto tcp from 10.0.0.0/24 to any port 2375:2376
| Aspect | OpenClaw | Clawdie + Docker VM | Clawdie + Jail |
|---|---|---|---|
| Isolation | None (shared process) | Full container isolation | Jail isolation |
| FreeBSD native | โ Yes | โ Needs Linux VM | โ Yes |
| Code changes | None | None | ~5 env vars |
| Agent spawn time | ~0.5s | ~2-5s | ~0.1s |
| Memory overhead | ~200MB | ~700MB (VM + container) | ~200MB |
| Skills system | Custom | Deterministic engine | Deterministic engine |
| Browser automation | Remote CDP | Built-in (container) | Built-in (jail) |
| Setup complexity | Low | Medium | Medium |
| ZFS snapshots | User dataset | VM disk image | Jail dataset directly |
| Choose | When |
|---|---|
| OpenClaw (Clawdie) | Maximum simplicity, native FreeBSD, no isolation needed, already have CDP browser |
| Clawdie + Docker VM | Want container isolation, zero code changes, okay with VM overhead |
| Clawdie + Jail | Want isolation + native FreeBSD performance, okay with small code changes |
This document and the associated configuration are open source. Fork it, adapt it, run your own instance.
git clone https://codeberg.org/Clawdie/clawdie.git
cd clawdie
| File | Purpose |
|---|---|
docs/nanoclaw-on-freebsd.html |
This document |
.env.example |
Template for environment variables |
.claude/mcp-servers.json.example |
MCP config template (Stripe, etc.) |
scripts/start-clawdie.sh |
tmux glass-pane launcher |
skills/freebsd-jail/ |
Jail setup skill (planned) |
.env โ Contains API keys, tokenscredentials/ โ Telegram session, OAuth tokensdata/ โ SQLite database, user datastore/ โ Message history*.pem, *.key# 1. Copy environment template
cp .env.example .env
# 2. Edit with your values
vim .env
# Required:
# - TELEGRAM_BOT_TOKEN (from @BotFather)
# - ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN
# Optional:
# - STRIPE_SECRET_KEY (for payments)
# - DOCKER_HOST (if using Docker VM)
# 3. Install dependencies
npm install
# 4. Build
npm run build
# 5. Start (choose one)
./scripts/start-clawdie.sh # tmux glass-pane
npm start # direct
# 6. Register your Telegram chat
# Send /chatid to your bot, then register via IPC
| File | Customize |
|---|---|
groups/main/CLAUDE.md |
Agent personality, instructions |
groups/main/IDENTITY.md |
Who the agent is |
groups/main/USER.md |
About you (the operator) |
.env |
ASSISTANT_NAME, timezone, etc. |
| Branch | Purpose |
|---|---|
main |
Production-ready, stable |
dev |
Work in progress, experiments |
freebsd-jail |
Jail-based isolation (experimental) |
# Check for accidental secrets:
grep -r "sk_live\|rk_live\|whsec_\|TOKEN=" . --include="*.ts" --include="*.js" --include="*.env"
# Verify .gitignore is working:
git status --porcelain | grep -E "\.env|credentials|\.pem|\.key"
# Should return nothing!