25 KiB
CLAWDIE-ISO — USB Installer Plan
Status: Active goal — bare-metal deployment path (complements cloud/VPS) — 17.Mar.2026
Target repo: codeberg.org/Clawdie/clawdie-iso (to be created)
Purpose
Create a bootable USB image that reduces a complete Clawdie-AI deployment to a single physical step: boot USB, answer a few screens, unplug USB, done.
The installed system is a plain FreeBSD + desktop + Clawdie-AI — nothing exotic, nothing persistent on the USB. The USB is a deployment vehicle only.
The install combines three procedures that currently run separately:
| # | Path | Tool | What it does |
|---|---|---|---|
| 1 | FreeBSD base | bsdinstall | Disk layout, ZFS, base OS, bootloader, root/user accounts |
| 2 | Desktop setup | desktop-installer | GPU detection, Xorg/Wayland, DE packages, login manager, audio |
| 3 | Clawdie-AI | setup.sh → install | Bastille jails, PostgreSQL, nginx/CMS, rc.d service, bot config |
Current state: these three are completely separate. An operator must install FreeBSD, reboot, install desktop-installer, run it, reboot (sometimes), run setup.sh, configure. Minimum 2 reboots, typically 3+ operator interventions.
Target state: one ISO, one bsddialog wizard, one reboot.
Desktop-Installer Reference
outpaddling/desktop-installer
is a FreeBSD ports package (pkg install desktop-installer) that automates the
post-install desktop configuration step:
- Detects GPU via
pciconf -lv - Installs the right kld modules (i915kms, nvidia-modeset, drm, vboxguest, etc.)
- Writes
kld_listto/etc/rc.conffor persistent driver loading - Installs the chosen DE (Lumina — FreeBSD-native)
- Configures dbus, CUPS, sound (OSS/PulseAudio), login manager (lightdm)
- Handles VirtualBox, VMware, Parallels, bhyve guest additions automatically
- Does NOT typically require a reboot — kld can be loaded live or on next boot
- Respects quarterly vs latest pkg repository configured on the system
We borrow the detection and configuration logic from desktop-installer but do not run the interactive script. Our build process will vendor the GPU detection function and call it from our own bsddialog wizard.
Reboot Analysis — Can We Do 1 Reboot?
Three potential reboot triggers:
Reboot 1 (mandatory): bsdinstall completes bsdinstall always reboots after base install. Can't be avoided. The system must boot from HDD before userspace can run.
Reboot 2 (optional): GPU driver loading
Most GPU drivers on FreeBSD are kernel modules (klds). desktop-installer writes
them to kld_list in rc.conf, but they can also be kldloaded into a live system.
The key question: can we detect the GPU and write correct config WITHOUT having
the module loaded yet?
Answer: yes. GPU detection uses pciconf -lv (reads PCI device tree, no kld
needed). The output gives us PCI device ID and vendor ID. We match against a
known table to select the right kld and write it to rc.conf. On first boot from
HDD, the kld loads automatically. X11/Wayland starts clean. No intermediate reboot
is needed to make this work.
VirtualBox is explicitly out of scope for this installer. Target: real hardware and bhyve only. VBox guest packages are not bundled on the USB image.
Reboot 3 (not needed): Clawdie setup
setup.sh and all jail provisioning steps run in userspace. No reboot needed.
rc.d service starts without reboot via service clawdie onestart.
Verdict: 1 reboot is achievable for all common hardware. Two reboots may still be the safer default for first release if GPU detection reliability is uncertain. See Options section.
Pkg Branch Selection
FreeBSD ships two pkg repository branches:
| Branch | URL suffix | Update cadence | Best for |
|---|---|---|---|
quarterly |
/quarterly/ |
Stable snapshots, security fixes only | Servers, production |
latest |
/latest/ |
Tracks ports HEAD, newest versions | Desktops, GPU drivers, Node24 |
Recommendation: default to latest. GPU drivers (drm-kmod, nvidia-driver),
Node 24, and many Bastille-related packages are updated sooner in latest.
Quarterly has caused GPU driver installation failures on new hardware.
How to implement the toggle:
During the install wizard (bsddialog), present a radiolist before any packages are installed:
Package repository branch:
(*) Latest — newest packages, best driver support [recommended]
( ) Quarterly — stable branch, security updates only
The selected branch sets /usr/local/etc/pkg/repos/FreeBSD.conf:
FreeBSD: {
url: "pkg+https://pkg.FreeBSD.org/${ABI}/latest",
mirror_type: "srv",
enabled: yes
}
For offline install from USB, the bundled pkg repo must match the selected branch.
Build the USB with latest packages. Add a note: if the user selects quarterly,
the offline install falls back to the USB-bundled packages (which are latest
builds) and the system upgrades to quarterly on first internet connection.
Simpler alternative: only support latest offline, add quarterly as a post-install
option in the wizard.
Options
Option A — Pre-configured silent install (1 reboot, no interaction)
All choices are baked into a config file on the USB (clawdie.conf). The operator
edits this file before booting:
PKG_BRANCH=latest
DESKTOP=lumina
AGENT_NAME=clawdie
ASSISTANT_NAME="Clawdie"
TZ=Europe/Ljubljana
bsdinstall runs from installerconfig. After base install, a post-install script
reads clawdie.conf, installs everything offline, writes all config. Reboot once
into a fully configured system.
Pro: Fully automated, reproducible, CI-friendly, no bsddialog needed Con: Requires USB preparation before boot. No interactive personalization. Reboots: 1 Best for: Reproducible deploys, CI testing, pre-provisioned hardware
Option B — Interactive bsddialog wizard in installerconfig (1 reboot, guided)
After bsdinstall's base install completes but before the system reboots,
installerconfig runs a bsddialog wizard. The wizard covers:
- Pkg branch (latest / quarterly)
- Desktop: Lumina (fixed, no choice)
- Agent name + assistant name
- Telegram bot token (optional, can be skipped)
- Summary screen → confirm → install
GPU is detected from the live USB environment via pciconf -lv before chroot.
Detection result is written into the target system's rc.conf. All packages are
installed offline from the USB repo. Clawdie setup runs in chroot.
Single reboot into configured system.
Pro: Best UX, matches existing bsddialog pattern, interactive but 1-step Con: bsddialog in post-install chroot requires careful path setup Reboots: 1 Best for: First install on new hardware, operator-configured deployments
Option C — rc.firstboot script (2 reboots, most reliable)
Standard bsdinstall base install. Minimal installerconfig just copies the
wizard script and enables a firstboot rc.d service. On first real boot from HDD
(reboot 1), the firstboot service runs the bsddialog wizard on the live system.
Packages install, GPU loads live via kldload, DE and Clawdie come up. Optional
second reboot (reboot 2) for clean DM start.
Pro: Runs on real hardware (not in chroot), GPU kldload works immediately, most reliable path, easier debugging Con: 2 reboots minimum. Wizard runs after first reboot, which can feel slow. Reboots: 2 Best for: Conservative first release, maximum hardware compatibility
Option D — Extended bsdinstall menus (1 reboot, most native)
Modify bsdinstall to add extra menu screens: pkg branch, DE selection, Clawdie agent config. The installer feels like a fully native FreeBSD install with Clawdie options integrated into the standard installer flow. Post-install runs silently based on selections.
Pro: Most seamless UX — everything happens inside one familiar installer Con: Requires forking/patching bsdinstall (sh scripts in /usr/libexec/bsdinstall). Fragile against FreeBSD updates. Significant maintenance burden. Reboots: 1 Best for: Polished v2.0+ release, not first iteration
Option E — Live USB + install-to-disk (2+ reboots, most impressive)
The USB boots as a live Clawdie desktop system. User can interact with a running agent demo before committing to install. An "Install Clawdie to Disk" button/command runs the installer. After install + reboot, the HDD system is fully configured.
Pro: Demo capability, validate hardware/agent before committing, impressive Con: Most complex build (~2–3GB image), live persistence layer, 2 reboots, significant extra build infrastructure Reboots: 2+ Best for: Demo/showcase events, evaluators who want to try before installing
Recommendation
Start with Option C (rc.firstboot, 2 reboots) for v1 of the repo.
Reason: GPU detection running on the real live system (not in chroot) eliminates the most common failure mode. The 2-reboot tradeoff is acceptable for a v1 that works reliably. The wizard runs on boot from HDD, using the same bsddialog infrastructure already in Clawdie's setup.sh — no new tooling needed.
Upgrade path to Option B (1 reboot) for v1.1:
Once we have Option C working and tested on multiple hardware configs, we port the
wizard to run in the installerconfig post-install chroot. At that point we drop
the firstboot service and the second reboot.
Skip Option D and E for now. Neither is worth the complexity at this stage.
Option A can be added as a --unattended flag on top of Option B/C.
Three-Path Merge Flow (Option C)
USB boot
└─ bsdinstall
├─ Disk partitioning (ZFS, GPT+EFI)
├─ Base system distribution
├─ Bootloader
├─ Root password
├─ User account (clawdie)
└─ installerconfig:
├─ Copy /usr/local/share/clawdie-iso/firstboot.sh to HDD
├─ Install clawdie-iso rc.d firstboot service
└─ Reboot from HDD
First boot from HDD (reboot 1)
└─ rc.firstboot (clawdie-firstboot service)
├─ bsddialog: pkg branch (latest [default] / quarterly)
├─ bsddialog: agent name + assistant name
├─ bsddialog: Telegram bot token (optional)
├─ bsddialog: summary + confirm
│
├─ [pkg] Write /usr/local/etc/pkg/repos/FreeBSD.conf (selected branch)
├─ [pkg] pkg install from USB offline repo (desktop-installer deps)
│
├─ [gpu] pciconf -lv → detect GPU vendor/device ID
├─ [gpu] kldload appropriate driver(s)
├─ [gpu] Write kld_list to /etc/rc.conf
├─ [gpu] Write /etc/X11/xorg.conf.d/ if needed
│
├─ [de] Install chosen desktop environment from USB repo
├─ [de] Enable dbus, hald, login manager in rc.conf
├─ [de] Configure .xinitrc / session for clawdie user
│
├─ [clawdie] Extract clawdie-ai.tar.gz to /home/clawdie/clawdie-ai
├─ [clawdie] Set up .env defaults (AGENT_NAME, ASSISTANT_NAME, TZ)
├─ [clawdie] cd /home/clawdie/clawdie-ai && npm run install
│ ├─ environment (host pkg baseline from USB)
│ ├─ jails (worker + db + git + cms)
│ ├─ db (PostgreSQL + pgvector)
│ ├─ skills-memory (if artifacts/skills.db present on USB)
│ ├─ cms (nginx + Astro)
│ ├─ service (rc.d clawdie)
│ └─ verify
│
└─ Disable clawdie-firstboot service (runs once only)
Running system (no reboot 2 needed for Option C baseline)
└─ Login manager starts → desktop session → clawdie agent running
Repo Structure (clawdie-iso)
clawdie-iso/
├── build.sh ← main build script
├── build.cfg ← version pins, image size, pkg list
├── installerconfig ← bsdinstall post-install hook
├── firstboot/
│ ├── firstboot.sh ← the bsddialog wizard + setup runner
│ ├── rc.d/
│ │ └── clawdie-firstboot ← FreeBSD rc.d service (runs once)
│ └── gpu-detect.sh ← pciconf → kld table lookup
├── packages/ ← pre-fetched .pkg files (gitignored, fetched by build.sh)
│ ├── .repo/ ← generated by pkg repo
│ └── *.pkg
└── CLAWDIE-ISO.md ← this document (copy)
build.sh responsibilities:
- Fetch FreeBSD memstick installer image (verified checksum)
- Fetch all required .pkg files to
packages/(node24, bastille, desktop-installer deps, etc.) - Run
pkg repo packages/to generate repo metadata - Unpack memstick image
- Inject into memstick:
installerconfig,firstboot/tree,packages/repo,clawdie-ai.tar.gz - Repack image
- Output:
clawdie-iso-YYYYMMDD.img
USB Package Manifest
Derived from setup/environment.ts host baseline + desktop-installer + DE packages:
Clawdie host baseline:
node24, npm, bsddialog, bastille, git, tmux, python311, uv,
ripgrep, fd-find, rsync, postgresql17-client, py311-pillow, dejavu
Xorg baseline:
xorg-minimal, xf86-video-intel, drm-kmod,
xf86-input-libinput, dbus, hal
Note: xf86-video-vboxvideo and VirtualBox guest additions are excluded.
Target hardware is real machines and bhyve only. VirtualBox is out of scope.
Lumina (sole supported DE):
lumina-core, lumina-themes, lumina-calculator, lumina-archiver,
lumina-filemanager, lumina-screenshot, lumina-open, openbox,
libxcb, libxdg-basedir, lightdm, lightdm-gtk-greeter
NVIDIA support (optional):
nvidia-driver, nvidia-settings
Total estimated size (Lumina path, latest): ~1.5 GB packages + ~600 MB FreeBSD base
Node.js Shared Modules Setup
A critical user-space tweak that must be reproduced exactly on the installed
system: npm's global package prefix must be set to the clawdie user's home
directory, not to /usr/local.
Why it matters: On FreeBSD, /usr/local is owned by root. Running
npm install -g as the clawdie user fails without this. The prefix setting
redirects globals to a user-owned path that is in PATH.
Current production setup (from /home/clawdie/.npmrc):
prefix=/opt/clawdie/npm-global
PATH addition (must be in /home/clawdie/.profile or .shrc):
export PATH="/opt/clawdie/npm-global/bin:$PATH"
firstboot.sh must:
- Write
/home/clawdie/.npmrcwithprefix=/opt/clawdie/npm-global - Create
/opt/clawdie/npm-global/(with correct ownership) - Symlink
/home/clawdie/.npm-global→/opt/clawdie/npm-global - Append PATH export to
/home/clawdie/.profile - Pre-populate the global npm cache from USB-bundled packages so that
npm installinside clawdie-ai runs fully offline
Build-time: build.sh must pre-fetch npm packages and bundle them as a
local npm cache tarball (npm-cache.tar.gz) to be extracted to
/home/clawdie/.npm during firstboot. This eliminates network calls to
registry.npmjs.org during install.
Bundled Agent CLIs (npm-globals)
In addition to the offline npm cache, the ISO ships three pre-packed agent
CLIs as standalone tarballs so the firstboot environment satisfies the
setup onboard agent-CLI prereq gate without network access:
@anthropic-ai/claude-code→claude@google/gemini-cli→gemini@mariozechner/pi-coding-agent→pi
Build path:
clawdie-iso/scripts/fetch-npm-globals.shrunsnpm packfor each package intotmp/npm-globals/build.shcopies that directory to${USB_SHARE}/npm-globals/firstboot/shell-npm-globals.sh(functionclawdie_shell_npm_globals_install) runsnpm install -g <tgz>as the clawdie user withnpm_config_prefix=/opt/clawdie/npm-globalso the binaries land in the user-owned prefix
codex is not in the npm bundle — it ships as the FreeBSD codex pkg
and is installed via pkg install -y codex. End-to-end validation of all
four CLIs in a fresh worker jail is in doc/AGENT-CLI-VALIDATION.md.
AGENTS.md Seeding (Planning)
AGENTS.md is the agent's identity and behavior file. It must be seeded during firstboot with values derived from the install wizard (assistant name, locale, agent domain, etc.).
Plan (not yet implemented):
- A template
AGENTS.md.tplis bundled on the USB inside the clawdie-ai tarball atgroups/global/AGENTS.md.tpl(orgroups/main/AGENTS.md.tpl) - During firstboot, after
.envis written,firstboot.shrenders the template by substituting:{{ASSISTANT_NAME}}→ value from wizard{{AGENT_NAME}}→ derived system name{{AGENT_DOMAIN}}→ configured domain{{ASSISTANT_LOCALE}}→ selected locale{{TZ}}→ selected timezone
- Output written to
groups/global/AGENTS.mdbeforenpm run installruns - This ensures the agent has a fully personalized identity from first boot
Scope: Template design and rendering are handled in the clawdie-ai repo.
The clawdie-iso repo only needs to call the render step during firstboot.
.env Generation Strategy
The installed system requires 65 environment variables to be set before Clawdie-AI can start. They fall into four categories:
| Category | Count | Source |
|---|---|---|
| Identity / locale | 8 | Human provides via wizard |
| API keys / tokens | 14 | Human provides (all optional / deferrable) |
| Auto-generated secrets | 9 | openssl rand -base64 32 at install time |
| Derived structural | 34 | Calculated from identity (jail IPs, names, etc.) |
Three options for how the wizard handles this:
.env Option 1 — Minimal wizard (5 questions, everything else silent)
The wizard asks only the irreducible human questions:
- Assistant name → derives
AGENT_NAMEautomatically - Timezone
- Domain name (default:
<agentname>.local) - Primary LLM provider + key (Anthropic / OpenAI / skip)
- Telegram bot token (or skip)
All 9 secrets: auto-generated silently with openssl rand.
All 34 structural vars: derived from identity choices (fixed subnet, fixed jail
IPs, names from AGENT_NAME, etc.).
API keys not asked: left blank in .env, operator fills post-install.
Pro: Fastest path. 5 screens. Least friction. Works completely offline.
Con: No visibility into generated credentials during install. Operator must
read .env to find database passwords. No way to customize subnet or features.
Best for: First-time users, fast re-deploys, CI/automated testing.
LLM Key Entry — Deferred to pi first-run (design decision)
LLM provider keys are not collected by the ISO installer wizard. This is intentional. The installer is decoupled from provider choice — keys can change, providers can rotate, and keys should not be baked into USB media.
Instead the wizard collects only the pi profile (which provider class: Anthropic / OpenAI / Ollama / skip). The key itself is entered the first time pi is used:
Path A — pi first-run prompt (TTY detected):
On the operator's first interaction with the agent (or on firstboot post-install
if a TTY is present), pi-config checks for the provider key. If missing and a
TTY is available: bsddialog password input → written to .env → service
restarted automatically.
Path B — manual .env edit (headless / operator preference):
Operator edits /home/clawdie/clawdie-ai/.env directly, sets the appropriate
key var (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.), then runs
service clawdie restart. pi-config detects the key is present and skips
prompting on all future runs.
Detection logic (to be implemented in clawdie-ai):
pi-config runs →
selected provider key var present in .env? → skip, continue
not present AND TTY available? → bsddialog password input → write .env
not present AND no TTY? → log instruction, continue (non-fatal)
This is a gap in the current codebase — the non-blocking warning exists but
the TTY-triggered bsddialog prompt does not. This is a scoped task for the
clawdie-ai repo, not the clawdie-iso repo.
Task (clawdie-ai): Add TTY-aware key prompt to setup/pi-config.ts:
check for TTY (process.stdin.isTTY), if true and key missing, run
bsddialog --passwordbox "Enter <PROVIDER> API key" ..., write result
to .env, emit restart.
.env Option 2 — Tiered wizard with credentials summary (recommended)
Tier 1 (always shown, 4 screens):
- Name + timezone + domain → derive/generate everything → write
.env
Tier 2 (shown with "Configure now? [Y/n]"):
- LLM provider selection radiolist → key input
- Telegram bot token input
Tier 3 (shown only if operator presses "Advanced"):
- Custom subnet base (default 10.0.0.0/24)
- Feature flags: Ollama (local inference), Gitea (self-hosted git), Management jail
- ZFS dataset layout (default vs Clawdie-optimized)
End screen — Credentials Summary:
┌─ Clawdie Installation Complete ──────────────────────────┐
│ │
│ Agent: Clawdie (clawdie) │
│ Domain: clawdie.local │
│ Timezone: Europe/Ljubljana │
│ │
│ DB password: Xy7qP2mR... (see /home/clawdie/.env) │
│ Telegram: ✓ configured │
│ LLM: Anthropic ✓ │
│ │
│ Press Enter to start installation. │
└───────────────────────────────────────────────────────────┘
Pro: Best balance. Guided but not overwhelming. Summary screen prevents lost credentials. Advanced tier serves power users without cluttering the default path. Con: More wizard screens to implement. Summary screen needs careful design. Best for: Standard production deployments, first installs on new hardware.
.env Option 3 — Pre-boot config file on USB (clawdie.conf)
The USB image contains a clawdie.conf template file on a FAT32 partition that
is readable/writable from any OS (Windows, macOS, Linux, FreeBSD). Operator
edits it before booting:
# Edit this file on the USB before booting.
# Leave a field blank to be prompted during install.
ASSISTANT_NAME=Clawdie
TZ=Europe/Ljubljana
AGENT_DOMAIN=clawdie.local
PKG_BRANCH=latest # latest or quarterly
DESKTOP=lumina # lumina only
TELEGRAM_BOT_TOKEN= # optional, leave blank to skip
ANTHROPIC_API_KEY= # optional
Firstboot reads clawdie.conf first. Blank required fields trigger bsddialog
prompts. Blank optional fields are silently skipped. All secrets still
auto-generated regardless. A fully pre-filled clawdie.conf = zero-interaction install.
Pro: Supports fully automated reproducible deploys without modifying ISO. Familiar to sysadmins (edit a config file → boot). Works offline. Can be scripted or templated externally. Con: Operator must mount USB on another machine before install (FAT32 partition is accessible but requires extra step). Blank fields still need wizard. Best for: Reproducible testing, staging/production fleet deployments, CI.
Open Questions
-
GPU detection table: desktop-installer uses a shell function with a PCI ID lookup table. We should vendor
gpu-detect.shfrom desktop-installer source rather than reimplementing it. Verify license (BSD 2-clause — fine). -
pkg offline vs online: If target machine has internet,
pkg installwill upgrade from online repo after we've pointed at USB. That's fine and expected. The USB repo is only for air-gapped or offline installs. -
ZFS layout: Should we mirror bsdinstall's default ZFS layout or enforce a Clawdie-specific layout (separate datasets for bastille jails, var/db)? Decision needed before writing
installerconfig. -
ARM64 / Raspberry Pi: build.sh should detect host arch and fetch the matching FreeBSD image. For Pi, memstick is replaced with a different image type. Scope for v1: amd64 only, Pi deferred.
-
clawdie user UID: bsdinstall creates the user interactively. Should we override/enforce UID 1001 for
clawdie? Or let operator choose during bsdinstall and then reference$CLAWDIE_USERin firstboot.sh? -
Clawdie-AI source bundling: Bundle a tagged release tarball (clawdie-ai
vX.Y.Z) or pull from Codeberg during firstboot? Offline-first → bundle. build.sh should accept--clawdie-version X.Y.Zand fetch the tarball.
Next Steps
- Create
codeberg.org/Clawdie/clawdie-isorepo (empty, README only) - Copy this document as the repo's
CLAWDIE-ISO.md - Write
build.cfgwith FreeBSD 15.0-RELEASE-p4, amd64, Lumina default - Write
gpu-detect.sh(vendor from desktop-installer, simplify) - Write
firstboot.sh(bsddialog wizard + exec setup.sh) - Write
installerconfig(minimal, just copies firstboot/ tree) - Write
build.shskeleton (fetch → inject → repack) - Test: boot in bhyve VM on this host, validate firstboot flow