NookOS agent sessions, any machine

Self-hosted control plane

Run coding agents on every machine you own.

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.

  • Apache-2.0
  • Self-hosted
  • No telemetry
  • Rust + React

Try it

A shell that is not on this computer.

Simulated, in your browser — but the commands are the real ones.

you@laptop — demo shell simulated · nothing leaves your browser
# 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

This is already how the work gets done.

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

Four commands is the whole protocol.

Name a repo and a runtime. NookOS finds a machine that has it and starts the session there.

nook start

Open a session on any machine in the fleet.

$ nook start acme/checkout-api --runtime claude
 api — claude on azul

nook send

Type into it, without waiting around.

$ nook send api 'run the tests'

nook read

Look at its screen — now, or hours later.

$ nook read api --lines 200

nook exec

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

A control plane for machines, workspaces and sessions.

Claude writes the code. Nook decides where it runs, remembers that it ran, and is still holding the session when you come back.

Nodes

Machines join with a token and connect outbound — no inbound SSH, no public ports.

Workspaces

A workspace is a repo, not a machine. The same repo on three nodes is one workspace.

Sessions

tmux-backed and persistent. They survive refreshes, network drops and restarts.

Runtimes

bash, zsh, claude, hermes, codex — a missing runtime fails fast, not mysteriously.

Board

Kanban that dispatches: a card becomes a worktree, a machine and a session.

Activity

Every action is a live event, and per-workspace notes keep context next to the work.

MCP surface

An MCP server at /mcp exposes the whole system to any agent that speaks tools.

Identity

Generic OIDC with PKCE, multi-tenant from the schema up. Tokens issued and revoked in the UI.

Desktop

The same app in a native window, talking to your control plane like any other client.

The application

Mission control, in amber.

One screen for every machine, every repo and every running session you own.

The NookOS dashboard: a dark amber-on-charcoal panel layout showing online nodes, recent workspaces, running sessions and a live activity feed.
DashboardNodes, workspaces, running sessions and the live activity feed on one screen.
A NookOS session view with Claude Code running live in the embedded terminal, mid-conversation inside a repository.
A session, liveClaude Code running in a persistent tmux-backed session, streamed into the browser.
The NookOS workspaces list, showing repositories with their branch, dirty state and which machines hold a checkout.
WorkspacesEvery repo NookOS knows about, and which machines hold a checkout of it.
The NookOS kanban board with Triage, Todo, In Progress and Done columns holding task cards.
BoardKanban that dispatches: a card becomes a worktree, a machine and a session.
The NookOS nodes page listing machines with live capacity bars for CPU, memory and active sessions.
NodesLive CPU, memory, load and session capacity, so you can see what can take the work.
The New Work modal in NookOS, offering to clone a repository, add a worktree, start a new project or open an existing workspace, with machine and runtime pickers.
New WorkClone, add a worktree, or open what's already there — then pick the machine and the runtime.
The NookOS settings page on the Access tokens tab, listing issued tokens with their scope and creation date.
Access tokensIssue a user token to drive the whole fleet from the CLI; revoke it in one click.

Install

Run the control plane wherever you like.

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.

MethodPostgresReach for it when
Docker ComposeIncludedyou want the fastest real install
Behind a proxyIncludedyou terminate TLS at Traefik or nginx
docker runBring your ownyour orchestration isn't Compose
systemdBring your ownyou don't want containers at all
KubernetesBring your ownyou already run a cluster

Docker Compose

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.

Behind a proxy

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.

docker run

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

systemd

No containers. nook server initsystemd + 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.

Kubernetes

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/.

Once it's up

  1. Add a machine

    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_...
  2. Teach your agents

    Installs the NookOS skill into every agent on the machine — Claude Code, Hermes — so they can drive the fleet too.

    $ nook skills install
  3. Desktop app (optional)

    The same UI in a native window, nothing extra on the server. Downloads are at the top and on the latest release.

Posture

Self-hosted means self-hosted.

PropertyHow it behaves
NetworkNodes dial out over WebSocket. No inbound SSH, no public ports, no reverse tunnel to a vendor.
TelemetryNone. There is no phone-home, no analytics endpoint, and no account required to run it.
CredentialsA node token can only act on its own machine. Only a user token drives other machines, and it lives 0600 in your home directory.
DataYour Postgres, your disks, your repos. Git stays the source of truth; NookOS coordinates, it does not own.
LicenceApache-2.0. Fork it, run it, embed it, sell services on it.
ExtensibilityRust owns the types: OpenAPI is generated from the code and TypeScript from OpenAPI, so third-party clients cannot drift.

Questions

The ones people actually ask.

Why not just ssh and tmux?

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.

Do sessions really survive?

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.

What happens when a node goes offline?

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.

Does it work with agents other than Claude?

--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.

Can an AI drive NookOS itself?

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.

What's not done yet?

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.

Is there a hosted version?

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.

Your machines, one control plane.

Free, open source, Apache-2.0, self-hosted, no telemetry. Clone it and have a fleet in about five minutes.