Architecture¶
Clauster is a FastAPI app whose app factory lives in app.py; the entry point is
clauster.__main__:main (clauster run). It renders an Alpine.js + Jinja2 +
Tabler UI from templates/ (with jinja2-fragments) and static/.
Module map¶
Key modules under src/clauster/:
| Module | Responsibility |
|---|---|
app.py |
FastAPI app factory; routes, middleware, cookie/session/WS wiring. |
__main__.py |
CLI entry point and subcommands (run, hash-password, hash-token, hash-metrics-token, api-token (issue/list/rotate/revoke), mcp, doctor, backup/restore/migrate, install-service, reap-environments, keepers, usage, config (with config reconcile), deps (list/install/uninstall optional extras), and the headless session commands start/stop/status/sessions/projects/logs/open). |
runner.py |
SessionRunner — spawn / stop / observe standard claude remote-control bridges. |
pty_keeper.py |
Sidecar that owns a true-resume (pty) bridge's PTY. |
discovery.py |
Project discovery under projects_root; ~/.claude.json paths. |
provisioning.py |
Project create + clone (with the clone/SSRF guards). |
trust.py |
The workspace-trust writer (atomic + flock-guarded ~/.claude.json). |
bridge_log.py |
Parse the bridge debug log. |
logstream.py |
Tail the bridge debug log for the WebSocket stream. |
redact.py |
ANSI-strip + ID/secret redaction for the WS stream. |
inspector.py |
claude agents --json cross-check — the liveness source. |
procutil.py |
psutil-based process introspection: liveness with PID-reuse defense (create-time + cmdline match) and the match-gated kill behind bridge rediscovery and hosted orphan recovery. |
auth.py |
Auth foundation (fail-closed; pure functions, no FastAPI import). |
config.py |
Config load, env-override, and validation (ClausterConfig). |
config_editor.py · config_write*.py |
Tier-A config-editor write backends — read/validate/write the runtime claude config surfaces (settings, permissions, hooks, MCP, plugins, skills, subagents) from the dashboard, behind the config_write gates. |
login_shepherd.py · login_status.py |
Dashboard claude account login flow — subscription login (plain pipes) and long-lived setup-token (under a POSIX PTY or a Windows ConPTY, reusing PtyScreen), plus the account login-status probe surfaced in /healthz; behind the login_shepherd gates. |
db/ |
Persistence layer: engine.py (resolves the SQLite clauster.db URL under state_dir), models.py/stores.py (SQLAlchemy schema + record stores), bootstrap.py (startup Alembic-to-head + one-time legacy-JSON import, both fail-closed), and the packaged Alembic migrations/; the stores.py layer includes an append-only session_events lifecycle history (SessionHistoryStore) that backs the Projects last-used / cost sort. |
state.py |
Legacy state.json store — now an import source only (the live store is clauster.db). |
models.py |
Domain models. |
metrics.py |
Per-bridge resource sampling (CPU / memory / disk). |
usage.py |
Token + approximate-cost rollup from session transcripts. |
notify.py |
Outbound lifecycle notifications via Apprise (optional notify extra). |
webhooks.py |
Outbound HTTP lifecycle webhooks (opt-in extension seam) — POST on spawn/ready/stop/crash plus bg-settled/permission-needed/clone-done, fail-open, with an opt-in SSRF deny-list. |
prometheus.py |
Text exposition for the opt-in read-only /metrics endpoint. |
environments.py |
Server-side bridge-environment listing + reaper logic. |
supervisor.py |
Read / dispatch / stop Claude Code agent-view background sessions (claude --bg); backs the background-agents panel + /api/agents. |
claustrum_client.py |
Async unix-socket NDJSON JSON-RPC client for the claustrum daemon (hosted live-view channel; experimental). |
claustrum_daemon.py |
Connect-or-spawn lifecycle + auth-token management + health for the per-deployment claustrum daemon (experimental). |
hosted.py |
Hosted-channel session engine — HostedSession (stream-json spawn, stdout control-plane routing, redaction + ring buffer + fan-out, fail-closed permission parking) and HostedManager (registry, separate from the bridge runner). Has a live-view + permissions UI and reattaches across restarts; experimental. |
hosted_state.py |
Legacy hosted_state.json store for hosted sessions, keyed by claustrum_process_id — now an import source only (hosted-session records live in clauster.db, letting a Clauster restart reattach them). |
hooks/resume_recap.py |
The SessionStart hook that recaps the prior conversation into a restarted bridge. |
The two bridge modes¶
A bridge is a claude process Clauster launches in a project directory. The two
modes have different argv and different readiness logic and are deliberately
not unified.
Session modes (canonical vocabulary)¶
These four names describe how clauster spawns and talks to a claude process. Use
exactly these display names in UI labels, tooltips, help/offcanvas copy, and docs;
the serialized wire tokens (standard, pty, hosted) never change.
- Server Mode — a headless
claude remote-controlserver (the subcommand form) that hosts multiple Desktop/web sessions at once and survives a clauster restart, but does not resume a prior conversation. (wire token: launch/resume mode"standard") - Interactive Session — a single
claude --remote-control(flag form) bridge run under a PTY keeper, pairing a live terminal with remote control so it can genuinely restore the prior conversation on restart (true resume). (wire token: launch/resume mode"pty") - Background Agent — a fire-and-forget
claude --bgagent-view run that executes a task on its own and reports back, with no attached interactive session. (the--bg/ agent-view substrate) - Direct Session — clauster drives
claudeitself over the claustrum-daemon stream-json channel and renders the conversation in its own UI, rather than proxying a remote-control bridge. (wire token: session channel"hosted")
Reference: https://code.claude.com/docs/en/remote-control — Server mode is the
claude remote-control multi-session server; an interactive session is
claude --remote-control, a terminal paired with remote control.
Orthogonal to these modes is the experimental Direct Session channel (
hosted.py, wire tokenchannel"hosted"): a headless stream-jsonclauderun on the claustrum daemon's pipes rather than as a remote-control bridge. It is a separatechannelaxis on the instance model, not a third bridge mode. It has its own dashboard panel — start a session, watch it stream live, drive it, approve/deny tool prompts, and resume it after a restart — backed byWS /ws/hosted/{id}andGET /api/hosted.The key user-visible difference: a bridge (
remote-control/ pty) is cloud-visible — attachable fromclaude.ai/codeand the Claude mobile app. A Direct Session is local live-view only — it streams in this dashboard but is never attachable from the Claude app. Starting a Direct Session and then trying to open it on your phone is a dead end by design.With
claustrum.keep_children(default on) the daemon is spawned with-keep-children, so a daemon restart or upgrade leaves hosted agents running: on reconnect Clauster reattaches the sessions it can, and marks survivors it can no longer drive as recoverable orphans (Kill / Resume from the dashboard). A survivor is hard-killed only when it is provably still the same process — alive, recorded create-time match, hosted cmdline (procutil.is_killable_hosted); without that evidence it is reported lost, never killed.
Server Mode (claude remote-control)¶
The default (Server Mode). runner.py's SessionRunner spawns the headless
claude remote-control subcommand server:
- Multi-session — multiple Claude sessions per bridge.
- Survives a Clauster restart, but has no conversation resume — a restart
spawns a fresh, empty context window. For continuity, the opt-in
claude.resume_recapSessionStart hook recaps the most recent prior transcript into the new session. - Readiness is gated on the bridge registering an environment within
claude.startup_grace_seconds. A bridge that launches but can't authenticate to the controller stays alive yet never becomes connectable — liveness alone is not "running".inspector.pycross-checksclaude agents --jsonas the liveness source.
Interactive Session (claude --remote-control under a keeper)¶
Opt-in via claude.launch_mode: pty. On POSIX it runs under a real pty; on Windows
under a ConPTY keeper (pywinpty, the pty extra), falling back to Server Mode when
that extra is absent — see Platform support. pty_keeper.py runs
the claude --remote-control flag form under
a PTY keeper sidecar:
- Single-session.
- Genuinely restores prior conversation context on Resume (
--continuetrue resume) — it restores rather than recaps. - The keeper owns the PTY and outlives a Clauster restart; it is stopped by signal.
The mode is recorded on a bridge's instance at launch — claude.launch_mode
seeds new bridges only and never re-modes a running or stopped one. Stop and
resume always honour the recorded mode.
pty readiness
Newer claude flag-form builds stopped printing the
claude.ai/code/session_… connect URL, so pty readiness/ownership is gated
on liveness rather than on parsing that line.
Bridge lifecycle¶
- Spawn. The
claudebinary is resolved to an absolute path and the project name is validated before any subprocess; argv is always a list (nevershell=True). Before the first spawn Clauster acknowledges remote control in~/.claude.json(auto_enable_remote_control) and, if the directory is untrusted, the workspace-trust writer setshasTrustDialogAcceptedfirst. - Readiness. The bridge must register an environment within
startup_grace_seconds; otherwise it is markedERROR. Liveness is cross-checked againstclaude agents --json. - Observe. The debug log is tailed (
logstream.py), sanitized (redact.py), and streamed over a WebSocket. Live CPU/memory/disk metrics are sampled from the process tree (metrics.py) while the bridge runs. - Stop / Resume. Stop signals the bridge. Resume relaunches it honouring its
recorded mode — standard re-spawns (optionally recapping), pty resumes the
keeper with
--continue.
Interactive Session bridges and systemctl restart
Interactive Session bridges survive a systemctl restart under the
KillMode=process unit install-service writes by default (the keeper is
detached and reattached on startup). They're only reaped under systemd's
default KillMode=control-group, which kills the whole service cgroup —
clauster doctor flags that case. A session lost to a cgroup reap still has its
transcript recoverable with claude --continue.
Configuration & state¶
config.pyloadsclauster.yml(search order +CLAUSTER_<UPPER_SNAKE_PATH>env overrides), applies the fail-closed validators, and produces a validatedClausterConfig. See Configuration.db/is the persistence layer:db/engine.pyresolves the SQLiteclauster.dbURL understate_dir(Clauster is SQLite-only, #796) anddb/bootstrap.pyruns the Alembic migrations to head on startup — fail-closed, refusing to start on a failed migration — then performs a one-time import of any legacystate.json/hosted_state.json(renaming each to*.imported).state.py/hosted_state.pyremain only as those legacy JSON stores and import sources.clauster migrateis a legacy helper that upgrades an olderstate.jsonto the current JSON schema (the database schema is migrated automatically at startup, above);clauster backup/restoretar thestate_dir(clauster.dbincluded) + config.
Platform support¶
Clauster runs on Linux, macOS, and Windows; a handful of runtime capabilities
are POSIX-specific or otherwise platform-bound. The full capability matrix —
what works where, and the mechanism behind each cell (ConPTY keeper via the
pty extra, Shawl service install, POSIX-only fcntl.flock, and so on) —
lives in Platform support. When a platform behavior
changes, update that matrix rather than restating the gap in another doc.
Conventions¶
- Fail closed, never silently. Auth gates default to denial; bridge-lifecycle
errors surface rather than collapse into a misleading state. No bare
except: passswallows. - Validate before spawning. Resolve binaries to absolute paths and validate
project names before any subprocess; pass list-argv, never
shell=True. - Style + docstrings enforced by ruff (
E/F/I/W/UP/B/S/D, 99 cols); the test suite gates coverage at 96% on all three OSes (Windows measures through.coveragerc-win, which excludes the POSIX/ConPTY code it can't run).