Self-hosted hub for the clawborrator multi-operator Claude Code platform.
6.3K
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
//admin//api/v1/.../channel (for Claude Code MCP clients)/cli (for the claw CLI)/data/hub_v1.db for sessions, tokens, events,
agents, channels, and webhook subscriptionsmcp__clawborrator__* tools when paired with
clawborrator-mcp >= 0.0.39# 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.
| Var | Required? | Default | Notes |
|---|---|---|---|
GITHUB_CLIENT_ID | yes (for login) | unset | From your GitHub OAuth App. See "GitHub OAuth setup" below. |
GITHUB_CLIENT_SECRET | yes (for login) | unset | From your GitHub OAuth App. |
PORT | no | 8787 | HTTP listen port inside the container. Map with -p as needed. |
HOST | no | 0.0.0.0 (in image) | Bind address. Default is correct for container use. |
DB_FILE | no | /data/hub_v1.db | SQLite file path. Mount a volume at /data for persistence. |
LOG_LEVEL | no | info | pino log level: trace debug info warn error fatal. |
MCP_SYNC_CALL_TIMEOUT_MS | no | 15000 | Cap on synchronous MCP tool round-trips. |
CLAW_PUBLIC_ASK_PER_IP_DAILY | no | 100 | Public-thread rate limit per IP per day. |
CLAW_PUBLIC_THREAD_MESSAGE_CAP | no | 200 | Max messages per public-thread before it locks. |
CLAW_PUBLIC_CONTEXT_TURNS_MAX | no | 40 | Max turns of context kept on a public-thread. |
CLAW_PUBLIC_CONTEXT_BYTES_MAX | no | 60000 | Max bytes of context per public-thread. |
CLAW_PUBLIC_PROMPT_BYTES_MAX | no | 6000 | Max bytes per public-thread prompt. |
CLAW_PUBLIC_CHAT_TAG_TTL_MS | no | 900000 | TTL on public-chat tag bindings. |
The hub writes to a single SQLite file at DB_FILE (default
/data/hub_v1.db). All state lives there:
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.
clawborrator-hub (anything you want)https://your-hub-hostname.example.comhttps://your-hub-hostname.example.com/api/v1/auth/callbackLocalhost dev works with http://localhost:8787 as both homepage
and http://localhost:8787/api/v1/auth/callback.
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.
Once the hub is up, clients (your Claude Code sessions running on laptops, workers, CI agents) need to know two things:
wss://hub.example.com/channelck_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.
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).
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.
node:22-bookworm-slim (multi-stage)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)linux/amd64 (arm64 not yet built)USER in the Dockerfile/usr/local/bin/node server/dist/server.js8787/data (SQLite), pre-chowned to 65532:65532docker exec -it <c> sh won't work; rebuild
against gcr.io/distroless/cc-debian12:debug which adds busyboxAttestations shipped with each image manifest:
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.
Once the hub is up and you've logged in via GitHub, open / for
orchard-chat. You'll see:
Handoff
cards from worker / validator agentsThe admin UI at /admin/ is for hub-wide ops: agent moderation,
webhook subscriptions, user management.
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.
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.
ladder99/clawborrator-worker:latest: headless Claude Code in a
container. Pair this hub with workers to drive remote CC sessions.clawborrator-mcp on npm: the MCP server that connects a Claude
Code instance to this hub.clawborrator-cli: command-line ops against the hub.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.
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.
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.
Content type
Image
Digest
sha256:35df820b1…
Size
59.3 MB
Last updated
28 days ago
docker pull ladder99/clawborrator-hub_v1