- Go 54.3%
- TypeScript 41.8%
- Shell 1.3%
- CSS 1.2%
- Makefile 0.8%
- Other 0.6%
- Daemons report ClaudeAuth/GitAuth capability flags in the Hello frame; stored on instances and exposed via the API. - The board shows a live pre-flight checklist until the fleet can run a task (box enrolled, Claude authorized, git credential, repo added) — checks verify themselves as daemons report in. Replaces the static onboarding empty-state. - Fleet cards gain per-box CLAUDE/GIT credential chips. - docs/PRD.md: scope contract from the consolidation session — wedge, locked decisions, v0.1 "phone-at-breakfast" / v0.2 "fleet brain" milestones, parked items with re-entry triggers. |
||
|---|---|---|
| .claude/skills/serval | ||
| .github | ||
| cmd | ||
| config | ||
| db | ||
| docker | ||
| docs | ||
| frontend | ||
| internal | ||
| routes | ||
| schedule | ||
| .env.example | ||
| .gitignore | ||
| AGENTS.md | ||
| assets.go | ||
| caracal-agentd | ||
| CLAUDE.md | ||
| DEPLOY.md | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| install-agent.sh | ||
| main.go | ||
| Makefile | ||
| openapi.json | ||
| README.md | ||
| serval.yaml | ||
| sqlc.yaml | ||
caracal-web
Self-hosted cloud coding agents on your own hardware and your own Claude subscription. Think Cursor background agents / Codex, minus the vendor: you enroll any Ubuntu box, dispatch tasks from a web dashboard, watch the agent work live, review the diff, and open a pull request on your own Gitea — all without anything leaving your network except the model API calls themselves.
Each enrolled box runs a small Go daemon that executes Claude Code in throwaway, hardened Docker sandboxes and streams the transcript back over a single dial-out WebSocket.
The loop
dispatch → work → review → iterate → ship
- Dispatch — pick a repo, write a prompt, choose a model. The task lands on the box's daemon (live, or queued in an outbox until it reconnects).
- Work — the daemon shallow-clones the repo onto an agent branch
(
caracal/agent-<id>), runs Claude Code in a sandbox (--memory 4g --cpus 2 --cap-drop ALL, non-root), and streams every thinking/tool/result step to the dashboard in real time. - Review — when the run changes files, the daemon commits, diffs against
the base branch, and pushes the agent branch. The diff renders in the run
view (per-file, collapsible, +/− stats); the task lands in
review. - Iterate — send a follow-up from the run page. The agent resumes its Claude session (state persists across sandboxes) on a fresh clone of its own pushed branch, so prior work carries over. Costs accumulate per task.
- Ship — one click opens a pull request on your Gitea via its API, with a backlink to the run.
Runs that change nothing simply finish as done — no branch, no noise.
How it fits together
Browser ──HTTP + SSE──► control plane (this repo) your box
Go API + WS gateway ◄──WS── caracal-agentd
Postgres (data + LISTEN/NOTIFY) │ docker run
Gitea API (PRs) ▼
claude -p (sandbox)
│ git push
▼
your Gitea
- The daemon dials OUT (NAT-friendly, zero inbound ports) and authenticates with a per-instance token minted at enrollment.
- Transcript frames flow daemon → gateway → Postgres (
pg_notify) → the browser's SSE stream. Nothing is polled that can be pushed. - URLs are self-configuring: install commands, the bootstrap script, and
the daemon's WebSocket URL are derived from the address each client actually
used (LAN IP,
localhost, tailnet, or domain — reverse-proxy aware).PUBLIC_URLexists only as a fallback.
Quickstart (local dev)
Requires go, bun, docker. The app is fully self-contained — the web
framework is vendored in internal/serval/; there is no CLI to install.
cp .env.example .env # fill APP_KEY (make key) and DATABASE_URL
make setup # bun install + start Postgres + migrate
make dev # API :3000 + frontend :5173 + worker + scheduler, hot reload
Open http://localhost:5173, register an account, and you're on the board.
make help lists every task.
Add an agent box
Dashboard → Fleet → Add machine mints a one-time enrollment token and prints a paste-ready one-liner (the URL is whatever host your browser used, so it works on a LAN out of the box):
curl -fsSL http://192.168.1.20:3000/i/<enrollment-token> | sh
# …add: | sh -s -- --harden
The bootstrap downloads the prebuilt caracal-agentd (checksum-verified, or
builds from source), installs a systemd service, builds the sandbox image, and
prompts for credentials. --harden firewalls the box and enables unattended
security upgrades. Details: DEPLOY.md.
Credentials — who holds what
| Credential | Lives | Used for |
|---|---|---|
CLAUDE_CODE_OAUTH_TOKEN (claude setup-token, uses your Pro/Max plan) or ANTHROPIC_API_KEY |
box daemon env (/etc/caracal-agent/env, 0600) |
forwarded into each sandbox for Claude Code |
CARACAL_GIT_TOKEN (+ optional CARACAL_GIT_USERNAME) |
box daemon env | clone private repos, push agent branches — injected via an inline git credential helper, never in argv, never in the DB, never inside the sandbox |
GITEA_URL / GITEA_TOKEN |
server env (.env) |
the "Open PR" action (Gitea REST API) |
The sandbox container only ever sees the Claude credential; git operations happen host-side on the box.
Self-host
One image, role-based commands (serve / queue:work / schedule:run /
migrate):
cd docker
cp ../.env.example .env # fill APP_KEY (make key), optionally GITEA_*
docker compose -f compose.selfhost.yml up -d --build
For a LAN-only deployment nothing else is needed — access the dashboard by the server's LAN IP and every generated URL follows. The only egress the boxes need is the Anthropic API and your git host.
Layout
- Backend (Go): handlers in
internal/handlers, DI ininternal/providers, routes inroutes/. Daemon↔gateway protocol:internal/wire. WS hub + SSE fan-out:internal/gateway. Gitea client:internal/gitea. Request-derived URLs:internal/requrl. SQL indb/queries/*.sql(make sqlc), migrations indb/migrations. - Framework: vendored at
internal/serval/(chi router with typed handlers, sqlc/pgx, samber/do DI, River queues, goose migrations, argon2id auth + sessions). It's part of this codebase — edit it deliberately. - Daemon (Go):
cmd/caracal-agentd+internal/agent— dial-out WS client, sandbox lifecycle (sandbox.go), git finalize/push (ship.go), installer subcommands. Embeds the sandbox Dockerfile. - Frontend (TanStack Start + Tailwind):
frontend/(bun). Hand-written typed client insrc/lib/api.ts, live transcript viaEventSource, diff viewer insrc/components/diff-view.tsx. - Dev supervisor:
cmd/dev(make dev) — rebuild on save, sqlc on query change, vite alongside.
Verify
make check # go build + vet + frontend typecheck (the CI gate)
make test # Go tests, including the vendored framework's
go run . routes