All roots are resolved by hal0.config.paths and are HAL0_HOME-aware — see
Environment variables. Production layout (with
HAL0_HOME unset) follows the FHS:
| Root | Path | Purpose |
|---|---|---|
usr_lib() |
/usr/lib/hal0/current |
Code — a symlink to the currently active versioned install dir. |
lib() |
/usr/lib/hal0 |
Shipped read-only tree: bin/ wrappers, versions/, hooks. |
etc() |
/etc/hal0 |
User-editable config, preserved across updates. |
var_lib() |
/var/lib/hal0 |
Mutable runtime state, preserved across updates. |
var_log() |
/var/log/hal0 |
Optional supplementary log files — journald is the primary log sink. |
With HAL0_HOME=$X set (dev installs, tests): usr_lib = $X/usr-lib/hal0/current,
lib = $X/usr-lib/hal0, etc = $X/etc/hal0, var_lib = $X/var-lib/hal0,
var_log = $X/var-log/hal0.
/etc/hal0/ layout
/etc/hal0/
├── hal0.toml # main config
├── api.env # 0600, systemd EnvironmentFile — secrets/keys
├── openwebui.env
├── hardware.json # probe snapshot
├── manifest.json # release manifest, image digest pins
├── profiles.toml # optional — seeds apply if absent
├── stacks.toml # optional
├── slots/ # one <name>.toml per slot
├── agents/<name>.toml # per-agent MCP allowlist/config
└── *.lock # advisory RMW lock siblings
api.env is written with mode 0600 — it carries the admin/client bearer keys and
other secrets. See Config schema for every file’s
key layout.
/var/lib/hal0/ layout
/var/lib/hal0/
├── hal0.db # primary SQLite — registry, PortAuthority, metrics, runtime state
├── activity.db(+wal/shm) # audit trail SQLite
├── .first-run.lock # 0600, single-use OTP for first-run claim
├── .first_run_done # first-run completion sentinel
├── registry/
│ └── registry.toml(+.lock) # model registry
├── models/ # default model cache (unless overridden — see below)
├── slots/<name>/ # per-slot working dir
├── agents/
│ ├── secret.bin # 0600 HMAC secret for the agent chat proxy
│ └── <name>/workspace/ # per-agent sandbox root (chroot/bind-mount target)
├── stacks/state.json # active-stack pointer + content hash
├── openwebui/ # OpenWebUI state
├── .hermes/runtime.json # Hermes runtime info (embed token, etc)
└── .config/flm/models/ # default FLM (NPU) model cache
Model store resolution
The default model cache is /var/lib/hal0/models. Resolution order:
HAL0_MODEL_STOREenv var, if set.[models].store(or the deprecated[models].pull_root) inhal0.toml, via
effective_store()./var/lib/hal0/models(themodels_dir()default).
[models].roots (default: just the effective store) plus the effective store itself,
deduplicated, form the full scan set (scan_roots()).
DEFAULT_MODEL_STORE = /mnt/ai-models is a historic convention for an external
NFS/fast-disk mount used as a slot container bind target — it’s not a resolver default,
just a documented convention some installs still use.
Other notable paths
| Path | Purpose |
|---|---|
~/.local/share/hal0/models |
XDG-style per-user model store — a UI display option only, not a resolver default. |
$PWD/.hal0ai (or $HAL0_PREFIX) |
Dev-mode install root, used by hal0 uninstall --dev. |
$HERMES_HOME (default ~/.hermes) |
Hermes agent home directory. |
Lock files
hal0 uses a generic sibling-lock pattern (hal0.config.locking.file_lock()):
<target>.lock next to any config file, advisory fcntl.flock.
| Pattern | Purpose |
|---|---|
/etc/hal0/*.lock |
Advisory read-modify-write locks on config writes. |
/var/lib/hal0/*.lock |
Advisory read-modify-write locks on state writes. |
/var/lib/hal0/registry/registry.toml.lock |
Registry cross-process write lock (sidecar file). |
<slots_dir>.lock |
Slot-config directory-level lock. |
<record>.lock |
Per-record lock for MCP installed.py read-modify-write. |
Temp file prefixes (atomic-write pattern)
hal0 writes config and state files via tempfile.mkstemp in the same directory,
fsync, then os.replace — so readers never see a torn write. Recognizable prefixes;
every one of these is transient — created, then either unlinked or renamed away,
never left on disk as a final artifact:
| Prefix | Used by |
|---|---|
.hal0-writeprobe-{pid} |
Write-permission probe (hal0.agents.hermes_provision.path_is_writable) — touched then immediately unlinked in the same call, to test whether an ancestor directory is actually writable (not just os.access-writable, which lies under SELinux/ACLs/NFS root-squash). Never renamed into place. |
.hal0-gpu-arbiter-*.tmp |
GPU arbiter state. |
.hal0-state-*.tmp |
Slot state / ID-keying migration. |
.hal0-stack-state-*.tmp |
Stack state. |
.hal0-env-*.tmp |
Atomic env-file writer (config/env.py). |
Persistent markers
These are not temp files — each is written once and left in place as the
final artifact, checked again on a later run. Don’t confuse them with the
atomic-write temp files above.
| Marker | Used by |
|---|---|
.hal0-managed |
Stamped into a claimed HERMES_HOME/agent data dir the moment hal0 takes ownership of it (hal0.agents.hermes_provision). It persists for the life of the install: hal0 agent uninstall rmtrees an agent home only when this marker is present, refusing to touch a directory (e.g. a user’s pre-existing ~/.hermes) that lacks it — see _safe_to_remove_data_dir at src/hal0/agents/manager.py:507 (called from manager.py:466). |
.hal0-build-stamp |
Content hash of the ui/ source tree, written under the served UI dist dir (${UI_DIST}/.hal0-build-stamp) right after a successful npm run build (installer/install.sh:1109). Persists across installs — the installer diffs it against the current tree hash on the next run and skips the rebuild only on an exact match. |
Portable export envelope formats
| Extension | Purpose |
|---|---|
.hal0profile.json |
Portable profile export/import envelope. |
.hal0stack.json |
Portable stack export/import envelope. |