Sign inSign up

ladder99/clawborrator-hub_v1

By ladder99

•Updated 28 days ago

Self-hosted hub for the clawborrator multi-operator Claude Code platform.

Image
0

6.3K

ladder99/clawborrator-hub_v1 repository overview

⁠clawborrator-hub_v1

Self-hosted hub for clawborrator⁠: the multi-operator Claude Code orchestration platform. Brokers WebSocket sessions between Claude Code instances, the orchard-chat operator UI, channel-side MCP servers, and the public agent registry.

Source: https://github.com/clawborrator/hub_v1⁠ License: MIT


⁠What you get when you run this image

  • An HTTP server on port 8787 that hosts:
    • The orchard-chat operator UI at /
    • The orchard-admin admin UI at /admin/
    • The REST API at /api/v1/...
    • The channel WebSocket at /channel (for Claude Code MCP clients)
    • The CLI WebSocket at /cli (for the claw CLI)
  • A SQLite database at /data/hub_v1.db for sessions, tokens, events, agents, channels, and webhook subscriptions
  • Drizzle migrations that auto-apply on every boot (idempotent)
  • A built-in OAuth flow for users to log in via GitHub
  • Pre-baked support for mcp__clawborrator__* tools when paired with clawborrator-mcp >= 0.0.39

⁠Quick start

# 1. Create a persistent volume for the SQLite database.
docker volume create clawborrator-hub-data

# 2. Run the hub.
docker run -d \
  --name clawborrator-hub \
  -p 8787:8787 \
  -e GITHUB_CLIENT_ID=<your github oauth app client id> \
  -e GITHUB_CLIENT_SECRET=<your github oauth app secret> \
  -v clawborrator-hub-data:/data \
  ladder99/clawborrator-hub_v1:latest

# 3. Verify it's healthy.
curl http://localhost:8787/healthz
# Expected: {"ok":true,"service":"hub_v1","db":"/data/hub_v1.db","ts":"..."}

If GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET are missing the hub will still boot, but any user attempting to log in via GitHub will hit a 503 with a clear message. You can defer setting them if all you want to test is the API surface.


⁠Required env vars

VarRequired?DefaultNotes
GITHUB_CLIENT_IDyes (for login)unsetFrom your GitHub OAuth App. See "GitHub OAuth setup" below.
GITHUB_CLIENT_SECRETyes (for login)unsetFrom your GitHub OAuth App.
PORTno8787HTTP listen port inside the container. Map with -p as needed.
HOSTno0.0.0.0 (in image)Bind address. Default is correct for container use.
DB_FILEno/data/hub_v1.dbSQLite file path. Mount a volume at /data for persistence.
LOG_LEVELnoinfopino log level: trace debug info warn error fatal.
MCP_SYNC_CALL_TIMEOUT_MSno15000Cap on synchronous MCP tool round-trips.
CLAW_PUBLIC_ASK_PER_IP_DAILYno100Public-thread rate limit per IP per day.
CLAW_PUBLIC_THREAD_MESSAGE_CAPno200Max messages per public-thread before it locks.
CLAW_PUBLIC_CONTEXT_TURNS_MAXno40Max turns of context kept on a public-thread.
CLAW_PUBLIC_CONTEXT_BYTES_MAXno60000Max bytes of context per public-thread.
CLAW_PUBLIC_PROMPT_BYTES_MAXno6000Max bytes per public-thread prompt.
CLAW_PUBLIC_CHAT_TAG_TTL_MSno900000TTL on public-chat tag bindings.

⁠Persistent state

The hub writes to a single SQLite file at DB_FILE (default /data/hub_v1.db). All state lives there:

  • Users, OAuth sessions, channel tokens
  • CC sessions (with their host, cwd, routing-name)
  • Events (tail/chat/permission/handoff timelines)
  • Files (operator-uploaded attachments; binary blobs)
  • Public-agent registry
  • Webhook subscriptions + delivery log

Always mount a volume at /data or you lose everything on container restart. The docker volume create step in the quick start handles this; for production you'd point -v /host/path:/data at a backed-up location.

Bind-mount permission note: the container runs as a non-root user (uid 65532). Named volumes inherit ownership from the image's pre-created /data template automatically. Bind mounts do not. If you -v /host/path:/data, you must first sudo chown -R 65532:65532 /host/path or the container will fail to write the SQLite file with EACCES.

Upgrading from :0.0.5 or earlier (which ran as root): existing named volumes are owned by root and the new nonroot container can't write them. One-shot fix before pulling the new image:

docker run --rm -v clawborrator-hub-data:/data --user 0 \
  alpine:latest chown -R 65532:65532 /data

Then proceed with the normal docker pull + docker run.

Upgrading on Fly.io (where the running image is distroless so the above alpine recipe can't shell in): pipe a node chown script through flyctl ssh instead.

echo 'const fs=require("fs"),path=require("path");
function walk(p){try{fs.chownSync(p,65532,65532)}catch(e){}
  if(fs.statSync(p).isDirectory())fs.readdirSync(p).forEach(c=>walk(path.join(p,c)))}
walk("/data");console.log("done");' | flyctl ssh console -a <your-app> -C /usr/local/bin/node

Then flyctl deploy. The bin/deploy-fly.sh⁠ script in the source repo wraps all of this (chown + deploy + healthz check) into one command.

Backup: sqlite3 is well-behaved with a hot copy; you can also just cp /data/hub_v1.db /backup/... while the container runs (worst case you lose the last few writes). For a quiesced backup, stop the container first.


⁠GitHub OAuth setup

  1. https://github.com/settings/developers⁠ -> "New OAuth App"
  2. Application name: clawborrator-hub (anything you want)
  3. Homepage URL: https://your-hub-hostname.example.com
  4. Authorization callback URL: https://your-hub-hostname.example.com/api/v1/auth/callback
  5. Click "Generate a new client secret"
  6. Copy the Client ID and Client Secret into the env vars above.

Localhost dev works with http://localhost:8787 as both homepage and http://localhost:8787/api/v1/auth/callback.


⁠Reverse proxy (HTTPS + WebSocket)

The hub speaks plain HTTP and plain WebSocket on port 8787. In production you put it behind a reverse proxy that terminates TLS and forwards both HTTP and WS upgrades.

Caddy (the easiest, auto-TLS via Let's Encrypt):

hub.example.com {
  reverse_proxy localhost:8787
}

Caddy upgrades to wss:// automatically; no extra config for WebSockets.

nginx:

server {
  listen 443 ssl http2;
  server_name hub.example.com;
  ssl_certificate     /etc/letsencrypt/live/hub.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/hub.example.com/privkey.pem;

  location / {
    proxy_pass http://localhost:8787;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_read_timeout 7200s;   # WS connections live long
    proxy_send_timeout 7200s;
  }
}

The Upgrade + Connection: upgrade headers are mandatory for /channel, /cli, and /supervisor. Without them clients fall back to HTTP and silently fail to register.


⁠Connecting clients

Once the hub is up, clients (your Claude Code sessions running on laptops, workers, CI agents) need to know two things:

  1. Where to connect: wss://hub.example.com/channel
  2. Auth: a channel token (ck_live_…) minted on the hub.

Mint one:

# Install the CLI (npm) on any machine.
npm install -g clawborrator-cli

# Log in (opens OAuth in browser).
claw login --hub https://hub.example.com

# Mint a channel token.
claw token mint --name my-worker

The output ck_live_… is what every Claude Code container/session uses as CLAWBORRATOR_TOKEN. See the worker_v1 README⁠ for the client-side env-var contract.


⁠Migrations

Drizzle migrations live in server/dist/db/migrations/ inside the image and apply automatically on startup. The migrator is idempotent: a re-run is a no-op against an up-to-date DB and takes ~10 ms.

If you upgrade by pulling a newer tag, a fresh docker run against the same volume will apply any new migrations before serving requests. There's no separate migrate command to run.

If a migration fails on boot (e.g. data corruption), the container exits with code 1 and the failure message is in docker logs. The DB is left untouched (Drizzle wraps each migration in a transaction).


⁠Upgrade

docker stop  clawborrator-hub
docker rm    clawborrator-hub
docker pull  ladder99/clawborrator-hub_v1:latest
docker run -d --name clawborrator-hub \
  -p 8787:8787 \
  -e GITHUB_CLIENT_ID=...  -e GITHUB_CLIENT_SECRET=... \
  -v clawborrator-hub-data:/data \
  ladder99/clawborrator-hub_v1:latest

The volume persists; the new container runs migrations against the existing DB on boot.


⁠Image internals

  • Build base: node:22-bookworm-slim (multi-stage)
  • Runtime base: cgr.dev/chainguard/glibc-dynamic:latest (5 MB, Wolfi-based, rebuilt daily so glibc CVEs clear within hours of upstream patches), plus a hand-installed upstream Node binary (currently 22.22.3, pinned in the Dockerfile)
  • Architecture: linux/amd64 (arm64 not yet built)
  • Default user: non-root (uid 65532), set via USER in the Dockerfile
  • Entrypoint: /usr/local/bin/node server/dist/server.js
  • Exposed port: 8787
  • Volume: /data (SQLite), pre-chowned to 65532:65532
  • Migrator runs at startup, before Fastify binds
  • No shell, no apt, no npm CLI in the runtime image (kills the picomatch / ip-address / dpkg / gnutls / systemd / libgcrypt / libcap / tar / sed CVE surface that comes with full Debian)
  • For live debugging: docker exec -it <c> sh won't work; rebuild against gcr.io/distroless/cc-debian12:debug which adds busybox

Attestations shipped with each image manifest:

  • SBOM (CycloneDX, all OS + npm packages with versions)
  • SLSA provenance (max mode: source repo digests, build args, Dockerfile hash)

Verify with docker buildx imagetools inspect ladder99/clawborrator-hub_v1:latest --format '{{json .SBOM}}'.

The build is fully reproducible from github.com/clawborrator/hub_v1⁠ using its Dockerfile.


⁠Operator UI tour

Once the hub is up and you've logged in via GitHub, open / for orchard-chat. You'll see:

  • Sessions list (left rail): every CC session connected via a channel token under your account
  • Chat pane (middle): live activity timeline for the selected session: prompts, replies, tool calls, permission requests, ask-question cards, and (for missions-style flows) Handoff cards from worker / validator agents
  • Side rail (right): per-session context: routing name, channel token, host, cwd, version, online status

The admin UI at /admin/ is for hub-wide ops: agent moderation, webhook subscriptions, user management.


⁠Public agent registry

Operators can publish a session as a public agent so other tenants can dispatch tool calls to it. Use the admin UI's "Agents" section, or the API:

curl -X POST https://hub.example.com/api/v1/agents \
  -H "Authorization: Bearer $CLAW_PAT" \
  -H "Content-Type: application/json" \
  -d '{"slug":"my-agent","tagline":"...","sessionId":"..."}'

Other operators then call your agent via mcp__clawborrator__dispatch_to_agent with handle <your-login>/my-agent.


⁠Health check

curl -fsS http://localhost:8787/healthz

Returns {"ok":true,"service":"hub_v1","db":"/data/hub_v1.db","ts":"..."}. Use in a docker healthcheck or k8s liveness probe.


  • worker_v1⁠ / ladder99/clawborrator-worker:latest: headless Claude Code in a container. Pair this hub with workers to drive remote CC sessions.
  • channel_v1⁠ / clawborrator-mcp on npm: the MCP server that connects a Claude Code instance to this hub.
  • clawborrator-cli⁠ on npm as clawborrator-cli: command-line ops against the hub.
  • worker_v1-missions⁠: toolkit for missions-style multi-agent runs against this hub.
  • worker_v1-managed-probe⁠: minimal one-role example of the ephemeral worker pattern.

⁠Troubleshooting

Container exits immediately with "migration failed". Check docker logs clawborrator-hub for the SQL error. Most common cause: a corrupted DB volume from an interrupted earlier write. Restore from backup or start fresh with a new volume.

GitHub login returns 503 with "oauth not configured". GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET are missing or empty in the container env. Re-run with both set.

Clients fail to connect to /channel (handshake error). Your reverse proxy isn't forwarding the WebSocket upgrade headers. See the nginx / Caddy snippets above; the Upgrade and Connection headers are mandatory.

Token validates but no session row appears. Bumping last_used_at happens at WS upgrade auth; the session row is created only after a valid register frame. If the client's clawborrator-mcp is older than 0.0.37 it cannot send the routingName field but registration should still succeed; if newer than 0.0.37 but the hub is older than the 947721c⁠ patch, the schema rejects the field silently. Pull this image (:latest has the fix) or upgrade.

Session shows @workspace-<uuid> instead of a meaningful name. The MCP client didn't send a routingName (cwd-derivation fallback fired). Set CLAWBORRATOR_ROUTING_NAME in the client's env to fix.


⁠Versioning

This image tracks clawborrator/hub_v1@main. :latest always points at the most recent build; numbered tags (:0.0.1, etc.) pin to specific package versions for reproducibility. Drizzle migrations are always forward-only; downgrading to an older image against a DB that's been migrated by a newer one is not supported.


⁠Source + contributing

https://github.com/clawborrator/hub_v1⁠

Open issues and PRs there. The Dockerfile in that repo is what built this image, and the build is reproducible.

Tag summary

Content type

Image

Digest

sha256:35df820b1…

Size

59.3 MB

Last updated

28 days ago

docker pull ladder99/clawborrator-hub_v1