Skip to content

Platforms

Where PersonalClaw runs and what to know per platform. The recommended install paths (uv tool, the bootstrap one-liner, Docker Compose) are the same everywhere — see Getting started. This page covers only the platform-specific gotchas.

Every row names the mechanism that proves it. No row claims “supported” without one, and the token points at something you can go read or re-run:

  • CI:<job> — a job in .github/workflows/ that runs the suite (or a release smoke) on that platform. Green on main/nightly is the evidence.
  • checklist:<section> — a documented manual walkthrough; evidence is a recorded run, not a CI job.
  • community — reported working by users; not verified by us.
PlatformSupportProof
Linux x86-64first-classCI:ci/test (ubuntu-24.04) + CI:breadth/ubuntu (ubuntu-24.04) + CI:release/images smoke (linux/amd64)
Linux arm64first-classCI:breadth/arm (ubuntu-24.04-arm) + CI:release/images smoke (linux/arm64)
macOS Apple siliconfirst-classCI:ci/test-macos (macos-26) + CI:breadth/macos (macos-26)
macOS Intelbest-effortcommunity — no Intel runner in CI; the x86-64 Python/wheel path is the same as Linux x86-64
Windows via WSL2supportedchecklist:Windows via WSL2 (this page)
Windows via Docker Desktopsupportedchecklist:Windows via Docker Desktop (this page) — written, not yet executed verbatim; the release runbook’s Windows checklist records the first run
Windows nativenot supported— see windows-native-audit

The arm64 rows became CI-backed with the arm jobs in what is now breadth.yml and the per-arch release smoke; before that they were aspirational.

The two CI:release/images smoke citations above were, for a while, a proof that had never run. The step shipped on 2026-08-10 and the last release before that was v0.1.3 on 2026-07-31, so it had executed zero times — and it invoked the container command through a login shell, which sources /etc/profile, which overwrites PATH instead of extending it, so the gateway image’s own ENV PATH was discarded and personalclaw was not found (exit 127, measured against the published 0.1.3 image). That is fixed, and the smoke now also requires the version the image reports to be the version being released (#3562). Worth stating plainly, because it generalises past this page: a gate that has never executed is not evidence, however precisely it is cited.

The matrix above is the backend. The desktop shell is narrower: every release attaches an unsigned Linux x86-64 AppImage and deb (CI:release/desktop-linux smoke) and an Apple-silicon macOS dmg, ad-hoc signed and not notarized (CI:release/desktop-mac smoke), and there is no Windows build — see the desktop guide for the per-OS signing story and the dated Windows deferral.


pip install 'personalclaw[models]' pulls the local-embedding stack. Wheel availability — not PersonalClaw — is what varies by arch. Read from the committed uv.lock (the resolver’s own record, so it stays honest as versions move):

Packagex86-64arm64 (macOS)arm64 (Linux)Note
faiss-cpu✅ wheel✅ macosx_14_0_arm64✅ manylinux_2_28_aarch64 + musllinux_1_2_aarch64musllinux wheel means Alpine works too
torch✅ wheel✅ macosx_14_0_arm64✅ manylinux_2_28_aarch64the lock takes Linux’s CPU build from PyTorch’s CPU index, so the image carries no CUDA; pip takes PyPI’s
sentence-transformers✅✅✅py3-none-any — pure Python, arch-independent

So [models] installs from wheels on every arch we claim — no source build, no compiler needed. Verify it yourself without an arm box:

Terminal window
python3 - <<'PY'
import re
for blk in re.findall(r'\[\[package\]\]\nname = "torch"(.*?)(?=\n\[\[package\]\]|\Z)',
open("uv.lock").read(), re.S):
print([w for w in re.findall(r'([\w.\-%]+\.whl)', blk) if "aarch64" in w or "arm64" in w])
PY

macOS arm64: faiss-cpu and torch ship two OpenMP runtimes

Section titled “macOS arm64: faiss-cpu and torch ship two OpenMP runtimes”

Both macOS wheels bundle their own copy of LLVM’s OpenMP runtime (faiss/.dylibs/libomp.dylib, torch/lib/libomp.dylib). Loading both packages is harmless; initializing the second runtime is not, and faiss initializes its copy the first time it enters a parallel region — its search, not its add. A process holding both therefore dies with OMP: Error #15 and SIGABRT on the next episodic dedup search rather than at import, which is why the symptom never points at the cause (#3324).

Consequences for how you run it on macOS:

  • Core stays torch-free on purpose. Nothing in personalclaw imports torch, faster_whisper or sentence_transformers; the sentence-transformers and faster-whisper apps own those dependencies. That is an enforced invariant, not a convention — tests/native_omp_guard.py fails the test that makes torch resident.
  • Installing sentence-transformers does not abort, for a reason you should not rely on. It imports sklearn, whose __init__ runs os.environ.setdefault("KMP_DUPLICATE_LIB_OK", "True") — the workaround LLVM documents as unsafe and able to produce silently wrong results. The pairing is suppressed there, not absent.
  • An in-process STT model app is the live hazard. faster_whisper brings torch without sklearn, so nothing sets that flag; with faiss also present, the gateway aborts on the next episodic write. Prefer a remote STT provider on macOS until that provider runs out-of-process.

KMP_DUPLICATE_LIB_OK=TRUE is not a supported configuration here: it turns a crash into undefined behaviour underneath your memory store.

The embedding stack, not the gateway, is what strains small boards. The gateway itself is light; torch + a loaded embedding model is the heavy part.

  • < 2 GB RAM — skip the extra. Install plain personalclaw and use a remote provider for embeddings. Everything except local embedding works unchanged.
  • 2–4 GB (Pi 4/5 class) — [models] can work, but add swap before first use; the model load is the spike, not steady state:
    Terminal window
    sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile
    sudo mkswap /swapfile && sudo swapon /swapfile
    # persist: echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
    Prefer a small model, and expect the first ingest to be slow.
  • ≥ 4 GB — no special handling.

If you hit an OOM kill during ingest rather than at startup, it is the model load — add swap or drop the extra; it is not a database or gateway problem.


Windows has no native build. The supported path is WSL2 (Windows Subsystem for Linux, version 2): a real Linux kernel inside Windows where PersonalClaw runs as an ordinary Linux install. The Windows-side browser reaches the dashboard through WSL2’s automatic localhost forwarding.

If you would rather not run a Linux shell at all, use Windows via Docker Desktop below — Docker Desktop itself uses a WSL2 backend, but you never touch the Linux shell. The rest of this section is for running PersonalClaw directly in WSL2.

From a WSL2 shell (Ubuntu or any distro), install exactly as on Linux — with uv, which downloads Python 3.13 if the distro has none:

Terminal window
uv tool install --python 3.13 personalclaw
personalclaw setup # interactive: workspace directory + timezone
personalclaw gateway

personalclaw doctor prints a platform: WSL detected line and tells you whether the background service will work (see below).

2. Keep your home on ext4, NOT on /mnt/c — this matters

Section titled “2. Keep your home on ext4, NOT on /mnt/c — this matters”

Store ~/.personalclaw/ on the WSL ext4 filesystem (i.e. under your Linux home, /home/<you>), not under /mnt/c (the mounted Windows drive).

The /mnt/c mount crosses the Windows/Linux filesystem boundary (a 9P network protocol), and small random I/O across it is dramatically slower — often 10-20x. PersonalClaw’s SQLite databases and FTS index do exactly that kind of I/O, so a home on /mnt/c makes chat history, memory, and search crawl.

Leave PERSONALCLAW_HOME unset (defaults to ~/.personalclaw) or point it at another ext4 path. Do not set it to a /mnt/c/... path.

3. Opening the dashboard (localhost forwarding + wslview)

Section titled “3. Opening the dashboard (localhost forwarding + wslview)”

WSL2 automatically forwards localhost between Windows and the Linux VM, so the dashboard URL the gateway prints (http://localhost:10000/...) opens directly in a Windows browser.

Started at a terminal, the gateway prints the URL prominently and then tries to open it. Inside WSL there is no Linux browser to launch, so PersonalClaw hands the URL to wslview (from the wslu package), which opens it in your Windows default browser. Most WSL distros ship wslu; if wslview is missing, install it (sudo apt install wslu) or just click the URL the gateway printed. Auto-open never blocks startup — a missing wslview is not an error. The background service below opens no browser: run personalclaw token for a sign-in link.

4. Background service needs systemd (opt-in on WSL2)

Section titled “4. Background service needs systemd (opt-in on WSL2)”

personalclaw service install registers a systemd unit so the gateway starts on boot and restarts on failure. WSL2 runs systemd only when you opt in. Enable it once:

  1. Create or edit /etc/wsl.conf inside your distro:

    [boot]
    systemd=true
  2. From Windows (PowerShell or CMD), fully restart the distro so the change takes effect:

    Terminal window
    wsl --shutdown

    Reopen your WSL shell. personalclaw doctor should now report service: systemd active.

Without systemd the background service will not persist. In that case, either run the gateway in a foreground shell (personalclaw gateway) whenever you need it, or start it on Windows login via Task Scheduler with a wsl -d <distro> -- personalclaw gateway action.


The no-Linux-shell path: Docker Desktop runs the published multi-arch images, so you never install Python or open a WSL prompt. You do need Docker Desktop with its WSL2 backend (its default; the legacy Hyper-V backend is not tested).

From a checkout:

Terminal window
git clone https://github.com/PersonalClaw/PersonalClaw.git
cd PersonalClaw
copy .env.example .env

Or with just the compose file, no checkout at all:

Terminal window
curl.exe -fsSLO https://raw.githubusercontent.com/PersonalClaw/PersonalClaw/main/deploy/compose/compose.yaml
# optional: put a .env next to it

Open .env and set any provider keys you use. All of them are optional, and with none set, first-run setup can still download the small default model. Paths in .env must be container paths, not Windows paths — the gateway runs inside Linux, so C:\Users\you\... means nothing to it. Leave PERSONALCLAW_HOME alone; compose already sets it to /data, backed by a named volume.

Where the .env goes: compose.yaml declares two candidate locations and marks both required: false — ./.env beside the compose file (the standalone layout) and ../../.env, the repo root (the from-a-checkout layout). Whichever exists is loaded; if the repo root also has one it wins, since a later env_file entry overrides an earlier one. A relative env_file path always resolves from the compose file’s parent directory, never from your shell’s cwd.

Before v0.2 the file declared a bare env_file: ../../.env, and required defaults to true in the Compose spec — so a copy of compose.yaml on its own did not “silently skip” your keys, it failed to start at all with env file /path/.env not found. That is fixed; both locations are now optional.

Terminal window
docker compose -f deploy/compose/compose.yaml up -d

First run pulls both images (personalclaw-gateway, personalclaw-web). Pin a release instead of latest by setting PERSONALCLAW_IMAGE_TAG in .env.

Ports are published on loopback only (127.0.0.1), which is what you want on a laptop:

URLWhat
https://localhost:3443the dashboard (HTTP/2; SSE + WebSocket streams)
http://localhost:3000308-redirects to the HTTPS port above
http://localhost:10000the gateway API directly

The HTTPS certificate is self-signed out of the box, so the browser shows a warning on first visit — expected; click through. (Mount a real cert over /etc/nginx/certs/personalclaw.{crt,key} to replace it.)

Docker Desktop forwards published ports to Windows localhost automatically, so no port-proxy or firewall rule is needed for loopback access.

4. Volume semantics — use the named volume, not a bind mount

Section titled “4. Volume semantics — use the named volume, not a bind mount”

State lives in the personalclaw_home named volume mounted at /data. Keep it that way on Windows. A bind mount from an NTFS path (-v C:\...:/data) crosses the Windows↔Linux filesystem boundary, and PersonalClaw’s SQLite databases do small random I/O plus file locking across it — which is both much slower and a known source of locking oddities. The named volume lives inside the WSL2 VM’s ext4 disk and behaves like native Linux storage.

Useful volume operations:

Terminal window
docker volume inspect compose_personalclaw_home # where it lives
docker compose -f deploy/compose/compose.yaml down # stop, KEEP the volume
docker compose -f deploy/compose/compose.yaml down -v # stop and DELETE state

Prefer personalclaw snapshot (run inside the gateway container) over copying the volume by hand:

Terminal window
docker compose -f deploy/compose/compose.yaml exec personalclaw-gateway personalclaw snapshot
Terminal window
docker compose -f deploy/compose/compose.yaml pull
docker compose -f deploy/compose/compose.yaml up -d

The named volume survives, so your state carries across the upgrade.

  • No personalclaw service install. Container restart policy replaces it — restart: unless-stopped already brings the stack back when Docker Desktop starts. Enable Start Docker Desktop when you log in for boot behaviour.
  • The desktop shell does not run on Windows (it ships for macOS and Linux — see the desktop guide) and is unrelated to this path.
  • Docker Desktop’s Hyper-V backend is untested; use WSL2.