pre-flight
Extracted & re-runnable
installer/lib/preflight.sh
gates systemd, Python 3.11–3.14, disk
(≥ 20 GB under /var/lib), and port collisions
(:8080, :3001) before any
apt/pip work.
install
The installer drops a systemd-managed hal0-api
on port :8080, probes your
hardware, writes a config file at /etc/hal0/,
and prepares the five built-in slots (primary,
embed,
stt,
tts,
img). Linux + systemd
only — non-interactive and idempotent.
curl -fsSL https://hal0.dev/install | bash
Runs as root via sudo — read the script first if you
haven't yet: installer/install.sh.
new in 2026-05-15
The install path was hardened end-to-end this cycle. ASCII banner
+ step counter + spinners up top; structured pre-flight checks
you can re-run any time with hal0 doctor;
hardware cards + a pre-populated slots/primary.toml
based on what was detected; contextual recovery hints on failure;
optional auth round-trip + a live "hello" + a QR for the dashboard
URL at the end.
pre-flight
Extracted & re-runnable
installer/lib/preflight.sh
gates systemd, Python 3.11–3.14, disk
(≥ 20 GB under /var/lib), and port collisions
(:8080, :3001) before any
apt/pip work.
probe
Hardware cards
After the venv lands, the installer prints four single-line
hardware cards via format_cards()
in src/hal0/hardware/probe.py
and writes /etc/hal0/hardware.json.
slot defaults
Auto-populated primary
recommend_primary_slot()
picks the largest curated chat model that fits and renders
slots/primary.toml with
enabled = false waiting for a model pull.
Operator-edited files are never overwritten.
recovery hints
Contextual ERR trap
A trap … ERR in
install.sh catches non-zero
exits and prints the failing step plus a focused hint —
disk full, missing build deps, blocked port, or a stale
venv — instead of dumping a raw bash trace at you.
finish
Live hello + QR + reachability
A live "hello" prompt streamed through the freshly-spawned
slot, a QR code for the dashboard URL rendered with
qrencode -t ANSIUTF8 when
available, and a reachability summary box on exit.
PATH
hal0 on PATH automatically
Idempotent
ln -sfn ${VENV_DIR}/bin/hal0 /usr/local/bin/hal0
during the CLI install step. No manual export, no
source /etc/profile.d/hal0.sh. Override the link
target with HAL0_PATH_LINK;
skipped in --dev and removed by
uninstall.sh.
re-run the pre-flight any time
hal0 doctor shells
the same installer/lib/preflight.sh
the installer sources, so you can audit a live host without
re-installing.
hal0 doctor
Flags: --plain forces ASCII output for log capture,
--ports "8080 3001 8186" tunes the port collision
check. Exit code matches the script's so it composes with
shell tooling (hal0 doctor && hal0 status).
The script is non-interactive. Every step is overridable by an environment variable so the same command works in CI, in a VM, and on your daily driver.
installer/install.sh:86). Checks for
python3, curl, systemctl,
and a writable prefix.
hal0 user and the FHS layout:
/usr/lib/hal0/current/ (atomic symlink to a
versioned dir), /etc/hal0/ (config — preserved
across updates), /var/lib/hal0/ (models,
registry, OpenWebUI state).
HAL0_PYTHON=/path/to/python3.12.
HardwareProbe().probe(), writes
/etc/hal0/hardware.json, prints four hardware
summary cards via format_cards(), and renders
/etc/hal0/slots/primary.toml with a recommended
backend + curated model via
recommend_primary_slot() if no primary file
exists. Skip the whole step with
HAL0_NO_PROBE=1.
hal0-api.service plus the
hal0-slot@.service template — one template, N
slot instances (no hand-written per-slot units).
openwebui.env with
OPENAI_API_BASE_URLS=http://127.0.0.1:8080/v1 so
the bundled chat UI lights up at :3001 with zero
extra config.
systemctl enable --now hal0-api. Slot units stay
offline until you assign a model in the dashboard or via
hal0 slot load.
Overrides accepted on the command line:
HAL0_PREFIX, HAL0_PORT (default
8080), HAL0_USER,
HAL0_PYTHON, HAL0_NO_PROBE, and
per-backend HAL0_TOOLBOX_IMAGE_* overrides for
pinning a specific Vulkan/ROCm/FLM/Moonshine/Kokoro image.
Prefer reading the script before running it? Clone the repo and
run the installer directly — same code path, no
curl | bash.
git clone https://github.com/hal0ai/hal0.git
cd hal0 sudo bash installer/install.sh
The same script the one-liner downloads. All
HAL0_* environment overrides apply here too:
sudo HAL0_PORT=18080 HAL0_NO_PROBE=1 bash installer/install.sh hal0 model pull is a stub today (501).
For the moment, drop a GGUF file into
/var/lib/hal0/models/ and point a slot at it:
sudo cp ~/Downloads/Qwen2.5-0.5B-Instruct-Q4_K_M.gguf \
/var/lib/hal0/models/
hal0 slot load primary --model qwen2.5-0.5b-instruct-q4_k_m Qwen 0.5B is the CI smoke model — it fits anywhere and verifies the slot lifecycle end-to-end (217–413 tok/s on Strix Halo iGPU, plenty fast on CPU for a sanity check).
Three checks: CLI is on PATH, the daemon is running, the API is responding.
hal0 --version Expected:
hal0 0.1.x
If the binary isn’t found, /usr/lib/hal0/current/bin
probably isn’t on your PATH — log out and
back in, or source /etc/profile.d/hal0.sh.
systemctl status hal0-api Expected (the key bits):
● hal0-api.service - hal0 OpenAI-compatible API
Loaded: loaded (/etc/systemd/system/hal0-api.service; enabled; preset: enabled)
Active: active (running) since ...
Main PID: 12345 (python)
Tasks: ...
Memory: ...
CPU: ... Active: active (running) is the line that
matters. Logs are in
journalctl -u hal0-api.
curl http://localhost:8080/v1/models Expected (empty registry on a fresh install):
{
"object": "list",
"data": []
}
After you load a model into a slot you’ll see it in
data[]. The dispatcher exposes the OpenAI surface
at /v1/*: chat, completions, embeddings, rerank,
audio transcription, audio speech. Slot ports (:8081–:8099)
are bound to 127.0.0.1 only — the API is the
single public surface.
The Vue dashboard ships on the same :8080 at
/; OpenWebUI is on :3001 prewired
against the local API.
# dashboard
xdg-open http://localhost:8080/
# prewired chat
xdg-open http://localhost:3001/
The hal0 uninstall CLI is on the v1 punch list but
not implemented yet — it currently returns a
NOT_IMPLEMENTED stub. Until that lands, do it by
hand:
# stop + disable units
sudo systemctl disable --now hal0-api
sudo systemctl disable --now 'hal0-slot@*' 2>/dev/null || true
# remove unit files + reload systemd
sudo rm -f /etc/systemd/system/hal0-api.service \
/etc/systemd/system/hal0-slot@.service
sudo systemctl daemon-reload
# remove install tree + state
sudo rm -rf /usr/lib/hal0 /usr/lib/hal0-*
# remove config (skip this line if you want to keep your slot configs)
sudo rm -rf /etc/hal0
# remove data — models, registry, OpenWebUI state (skip to keep models)
sudo rm -rf /var/lib/hal0
# remove the system user
sudo userdel hal0 2>/dev/null || true /etc/hal0/ and /var/lib/hal0/ are split
on purpose: config is preserved across updates, data is the heavy
thing. Decide per-directory what you keep.
The handful of issues we hit most while bringing v1 up.
The installer hard-fails on non-systemd / non-Linux hosts
(installer/install.sh:86). macOS and Windows
aren’t supported in v1. WSL2 isn’t either — WSL
doesn’t expose systemd by default and the slot units
won’t boot. Run it inside a real Linux VM, LXC, or on
bare metal.
hal0-api binds :8080 by default.
Override at install time and the systemd unit picks up the
new port automatically:
sudo HAL0_PORT=18080 bash installer/install.sh
# then point clients at http://localhost:18080/v1
Slot ports :8081–:8099 stay
on 127.0.0.1 and don’t need to change.
Slot lifecycle state lives in
/var/lib/hal0/slots/<name>/state.json and
is mirrored to the dashboard over SSE. When a slot errors,
the journal has the real reason:
# tail the slot's journal
journalctl -u hal0-slot@primary -f
# or via the CLI (same data)
hal0 slot logs primary --follow
Common causes: toolbox image not pulled, model file missing
from /var/lib/hal0/models/, GGUF too big for the
VRAM / GTT carveout. The slot form in the dashboard surfaces
a fit warning before you load — check there first.
As of 2026-05-15 the vulkan, rocm,
moonshine, kokoro, and
comfyui toolbox images are pinned by sha256 in
hal0/manifest.json and pull cleanly. Only
flm is still unpublished —
toolbox_images.flm.digest is null.
Until that lands, the primary, embed,
stt, tts, and img
slots work out of the box; an FLM-backed slot will sit
offline.
POST /api/models/{id}/pull (HF streaming
download) is still 501 NOT_IMPLEMENTED — it’s
on the remaining-gaps list for v1.0. Workaround: download the
GGUF manually with huggingface-cli and drop the
file into /var/lib/hal0/models/:
pip install --user 'huggingface_hub[cli]'
huggingface-cli download \
Qwen/Qwen2.5-0.5B-Instruct-GGUF \
qwen2.5-0.5b-instruct-q4_k_m.gguf \
--local-dir /var/lib/hal0/models/
Then hal0 slot load primary --model qwen2.5-0.5b-instruct-q4_k_m.
The installer drops a symlink at
/usr/local/bin/hal0 → ${VENV_DIR}/bin/hal0
so the binary is on the default PATH on every
mainstream distro out of the box. If you don’t see
it, the symlink step printed a warning — re-run the
installer or recreate it by hand:
sudo ln -sfn /usr/lib/hal0/current/bin/hal0 /usr/local/bin/hal0
hal0 --version
Override the symlink location at install time with
HAL0_PATH_LINK=/opt/bin/hal0 sudo bash install.sh.
Dev installs (--dev) skip the symlink so the
dev tree stays self-contained.
Next stops if the verify checks all passed.