nook start
Open a session on any machine in the fleet.
$ nook start acme/checkout-api --runtime claude ✓ api — claude on azul
Self-hosted control plane
NookOS starts Claude Code, Codex or a shell on any node in your fleet and types into it for you — no ssh, no tmux, no ports. Sessions live on the machine, so closing your laptop never stops the work.
Try it
Simulated, in your browser — but the commands are the real ones.
# azul is a box across the room. or across the world. same command. $ nook start acme/checkout-api --node azul --runtime claude --name api ✓ api — claude on azul nook exec api 'your prompt' nook read api $ nook exec api 'what machine are you on, and what is this repo?' ❯ what machine are you on, and what is this repo? ● I'm on azul (linux), in /srv/work/acme/checkout-api — the payment capture and settlement service. Want me to run the tests? claude · running
Try ↑ for history · tab to complete
Not a mockup
An agent on one machine started a Claude Code session on a second, sent it prompts, read its answers, and shipped the work — no ssh, no forwarded port, no wrapper library. NookOS ships that ability as a skill any of your agents can install.
send types
read looks
exec does both and waits
How it works
Name a repo and a runtime. NookOS finds a machine that has it and starts the session there.
Open a session on any machine in the fleet.
$ nook start acme/checkout-api --runtime claude ✓ api — claude on azul
Type into it, without waiting around.
$ nook send api 'run the tests'
Look at its screen — now, or hours later.
$ nook read api --lines 200
Send and wait for the answer. It knows when the agent is done.
$ nook exec api 'what broke in CI?' ● the flaky retry test — want me to fix it?
Want to look around first? nook get nodes | workspaces | sessions — kubectl-style, straight from the control plane.
What it is
Claude writes the code. Nook decides where it runs, remembers that it ran, and is still holding the session when you come back.
Machines join with a token and connect outbound — no inbound SSH, no public ports.
A workspace is a repo, not a machine. The same repo on three nodes is one workspace.
tmux-backed and persistent. They survive refreshes, network drops and restarts.
bash, zsh, claude, hermes, codex — a missing runtime fails fast, not mysteriously.
Kanban that dispatches: a card becomes a worktree, a machine and a session.
Every action is a live event, and per-workspace notes keep context next to the work.
An MCP server at /mcp exposes the whole system to any agent that speaks tools.
Generic OIDC with PKCE, multi-tenant from the schema up. Tokens issued and revoked in the UI.
The same app in a native window, talking to your control plane like any other client.
The application
One screen for every machine, every repo and every running session you own.
Install
One script bootstraps any of them — it asks whether this machine is a control plane, a node, or a hand-off to Helm, then writes the deployment files for you.
$ curl -fsSL https://nookos.dev/install.sh | sh
Prefer to read it before piping it to a shell? It's ~300 lines, and downloads are verified against a published SHA-256.
| Method | Postgres | Reach for it when |
|---|---|---|
| Docker Compose | Included | you want the fastest real install |
| Behind a proxy | Included | you terminate TLS at Traefik or nginx |
docker run | Bring your own | your orchestration isn't Compose |
| systemd | Bring your own | you don't want containers at all |
| Kubernetes | Bring your own | you already run a cluster |
Brings its own Postgres. nook server init writes this file and an .env of unique secrets, then docker compose up -d starts it.
# docker-compose.yml — generated by `nook server init`. Tags are pinned. services: postgres: image: postgres:16-alpine restart: unless-stopped environment: POSTGRES_USER: nook POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: nook volumes: [pgdata:/var/lib/postgresql/data] healthcheck: test: ["CMD-SHELL", "pg_isready -U nook"] interval: 5s timeout: 5s retries: 30 control-plane: image: ghcr.io/nook-os/nook-control:v0.4.10 restart: unless-stopped env_file: .env volumes: - ./agent-certs:/etc/nook:ro # agent-listener cert; nodes pin its fingerprint depends_on: postgres: { condition: service_healthy } ports: - "8080:8080" # API + UI backend - "8081:8081" # nodes (mutual TLS) web: image: ghcr.io/nook-os/nook-web:v0.4.10 restart: unless-stopped environment: CONTROL_PLANE_ORIGIN: http://control-plane:8080 depends_on: [control-plane] ports: ["80:80"] volumes: { pgdata: }
Upgrades are deliberate: bump the pinned tag and docker compose pull && docker compose up -d. Migrations run in the control plane at startup.
The API is an ordinary HTTP router. The agent port is not: it must be a TCP router in passthrough mode. The control plane has to see each node's client certificate itself — terminating TLS in front breaks mutual authentication and the fingerprint pin together.
# Traefik labels — from `nook server init` → "Compose behind Traefik". labels: - "traefik.enable=true" - "traefik.http.routers.nook-api.rule=Host(`nook.example.com`) && (PathPrefix(`/api`) || PathPrefix(`/mcp`) || PathPrefix(`/healthz`) || PathPrefix(`/.well-known`))" - "traefik.http.routers.nook-api.entrypoints=websecure" - "traefik.http.routers.nook-api.tls=true" - "traefik.http.services.nook-api.loadbalancer.server.port=8080" # Nodes, by SNI, passed through untouched. - "traefik.tcp.routers.nook-agent.rule=HostSNI(`agent.nook.example.com`)" - "traefik.tcp.routers.nook-agent.entrypoints=websecure" - "traefik.tcp.routers.nook-agent.tls.passthrough=true" - "traefik.tcp.services.nook-agent.loadbalancer.server.port=8081"
Prefer nginx? Proxy the API normally; give the nodes a stream block, which forwards TCP without decrypting it.
server {
server_name nook.example.com;
location / { proxy_pass http://127.0.0.1:80; }
location /api/ {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
# Nodes — TCP, never decrypted.
stream {
map $ssl_preread_server_name $backend {
agent.nook.example.com 127.0.0.1:8081;
}
server { listen 443; ssl_preread on; proxy_pass $backend; }
}
Point agent.nook.example.com at the host directly. Any hop that terminates TLS — a CDN, another proxy in front — breaks node authentication, and the symptom is a certificate that is not the one you generated.
For orchestration that isn't Compose. Bring your own Postgres — the whole prerequisite is a role and a database, with its URL as DATABASE_URL in .env. The schema needs no action; migrations run at startup.
-- on your Postgres
CREATE ROLE nook LOGIN PASSWORD 'choose-something';
CREATE DATABASE nook OWNER nook;
# run-containers.sh — written by `nook server init` → "docker run".
docker network create nook 2>/dev/null || true
docker run -d --name nook-control --network nook --restart unless-stopped \
--env-file .env \
-v "$PWD/agent-certs:/etc/nook:ro" \
-p 8080:8080 -p 8081:8081 \
ghcr.io/nook-os/nook-control:v0.4.10
docker run -d --name nook-web --network nook --restart unless-stopped \
-e CONTROL_PLANE_ORIGIN=http://nook-control:8080 \
-p 80:80 \
ghcr.io/nook-os/nook-web:v0.4.10
No containers. nook server init → systemd + native binary writes a 0600 .env against a Postgres you already run, and stops there — you run the control plane under your own init manager with that env.
# .env — generated. The process reads these; DATABASE_URL is your Postgres. APP_ENV=production CONTROL_PLANE_BIND=0.0.0.0:8080 PUBLIC_BASE_URL=https://nook.example.com WEB_ORIGIN=https://nook.example.com DATABASE_URL=postgres://nook:...@db.example.com:5432/nook SESSION_SECRET=... # 32+ chars; rotating it signs everyone out SECRETS_KEY=... # at-rest encryption. LOSING THIS LOSES stored secrets MCP_TOKEN=...
# /etc/systemd/system/nook-control.service — example; the installer writes only .env
[Unit]
Description=NookOS control plane
After=network-online.target
Wants=network-online.target
[Service]
EnvironmentFile=/opt/nook/.env
ExecStart=/usr/local/bin/nook-control
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Serve the web app with the nook-web image (or any static host) proxying /api, /mcp, /healthz and /.well-known to the control plane on :8080.
External Postgres only — no bundled database. The chart consumes exactly one Kubernetes Secret, by reference; it never stores secret material itself. Needs Kubernetes ≥ 1.23, Helm 3 and an ingress controller.
# 1. Create the Secret the chart references (external Postgres + session secret). kubectl create secret generic nook-control-secrets \ --from-literal=DATABASE_URL='postgres://user:pass@db.example.com:5432/nook' \ --from-literal=SESSION_SECRET="$(openssl rand -hex 32)" # 2. Get starter values (writes nook-values.yaml, downloads nothing). curl -fsSL https://nookos.dev/install.sh | sh -s -- --k8s # 3. Install from the published chart. Add --version X.Y.Z to pin. helm install nook oci://ghcr.io/nook-os/charts/nook-control -f nook-values.yaml
# nook-values.yaml — nothing here is a secret. existingSecret: nook-control-secrets # the Secret created above config: publicBaseUrl: https://nook.example.com webOrigin: https://nook.example.com ingress: host: nook.example.com # TLS: reference a Secret, or drive cert-manager with annotations. # tls: { enabled: true, secretName: nook-tls } # Agent mTLS listener (:8081) — how external nodes join. Off by default; its # TLS terminates INSIDE the control plane, so anything in front must be L4/passthrough. agent: enabled: false # publicUrl: agent.nook.example.com:8081 # tlsSecret: nook-agent-tls
Keep credentials in Vault, Google Secret Manager or AWS? The External Secrets Operator syncs your backend into that same Secret — copy-adjust examples per backend live in charts/nook-control/examples/secrets/.
Nodes → add node hands you this with the token and fingerprint filled in. The machine connects outbound — nothing is exposed to the internet.
$ curl -fsSL https://<your-control-plane>/install.sh | sh -s -- --token nook_join_...
Installs the NookOS skill into every agent on the machine — Claude Code, Hermes — so they can drive the fleet too.
$ nook skills install
The same UI in a native window, nothing extra on the server. Downloads are at the top and on the latest release.
Posture
| Property | How it behaves |
|---|---|
| Network | Nodes dial out over WebSocket. No inbound SSH, no public ports, no reverse tunnel to a vendor. |
| Telemetry | None. There is no phone-home, no analytics endpoint, and no account required to run it. |
| Credentials | A node token can only act on its own machine. Only a user token drives other machines, and it lives 0600 in your home directory. |
| Data | Your Postgres, your disks, your repos. Git stays the source of truth; NookOS coordinates, it does not own. |
| Licence | Apache-2.0. Fork it, run it, embed it, sell services on it. |
| Extensibility | Rust owns the types: OpenAPI is generated from the code and TypeScript from OpenAPI, so third-party clients cannot drift. |
Questions
You can. You will also be managing hostnames, keys, jump hosts, VPN state, port forwards and a mental map of which box has which repo — and none of that is addressable by an agent without giving it shell credentials to everything.
NookOS gives you one verb per intent instead: name the repo, name the runtime, go. The machine is an implementation detail the control plane resolves.
Yes — they're tmux sessions on the node, not proxied processes. Your laptop closing, your terminal dying, the network dropping and the control plane restarting are all survivable. Come back hours later and nook read still prints the screen and its scrollback.
The session is marked as having an offline node and starts working again when the machine reconnects. The tmux session was never destroyed; only the transport went away.
--runtime is any executable the node reports. Claude Code, Codex and Hermes are the ones in daily use; a plain shell is a first-class runtime, and adding another is a matter of it being installed on the machine.
That's the point. There's an MCP server at /mcp covering the whole surface, and an agent skill that teaches the CLI. The built-in dispatcher recommends where work should run — it recommends, it never acts. Humans approve.
Multiple worktrees of the same app on one machine can collide on ports, and there's no automatic fix yet. Kanban federation to Jira, GitHub, Linear and Trello sits behind a provider trait but ships local-only today. The roadmap is in the repo, not in a press release.
A managed service is coming. The self-hosted build is not a crippled tier — it is the whole product, and it always will be. Join the waitlist if you'd rather someone else ran the control plane.
Free, open source, Apache-2.0, self-hosted, no telemetry. Clone it and have a fleet in about five minutes.