- TypeScript 75.7%
- Svelte 19.9%
- Shell 2.6%
- CSS 1.2%
- Dockerfile 0.3%
- Other 0.3%
Probing @modelcontextprotocol/server@2.0.0 reversed five things the issue specified. Two matter: createMcpHandler serves the 2026-07-28 revision with the stateless 2025 fallback the issue described by hand, so taking the bare transport would ship a 2025-only server; and no SDK helper checks the token audience, so RFC 8707 binding is entirely ours. Access tokens move from JWT to opaque. AccessToken.find hands the raw value to the adapter keyed on jti and formats/jwt.js exports no getTokenId, so the lookup can never resolve a JWT — which would leave the resource server verifying signatures itself and unable to see a revocation before expiry. |
||
|---|---|---|
| .forgejo/workflows | ||
| docs/superpowers | ||
| nexus | ||
| worker | ||
| .gitattributes | ||
| .gitignore | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| DEPLOYMENT.md | ||
| docker-bake.hcl | ||
| docker-compose.desktop.yml | ||
| docker-compose.yml | ||
| fixup.mjs | ||
| README.md | ||
Agent Nexus
Self-hosted webapp for spawning and managing dockerized Claude Code agents.
Connects to one or more git providers (Forgejo today, GitHub planned), lists your repositories, and spawns Nexus Worker containers — each running Claude Code under tmux with cloud Remote Control so you can attach from the Claude mobile/web app or docker exec from the host.
Quick start
# 1. Build images
docker build -t agent-nexus-worker:latest ./worker
docker build -t agent-nexus:latest ./nexus
# 2. One-time: bootstrap the shared Claude session.
# Remote Control refuses inference-only setup-tokens, so workers share the
# full-scope OAuth state produced by `claude auth login`. The bootstrap
# script runs it interactively and saves the result to a docker volume.
docker run --rm -it -v claude-session:/session agent-nexus-worker:latest \
/bootstrap-claude-auth.sh
# 3. Start Nexus + the docker-socket proxy
docker compose up -d
Then open http://localhost:3001/ and:
- Set a master passphrase (first run only — it encrypts provider tokens at rest)
- Unlock with that passphrase
- If the bootstrap step ran, the UI advances past the Bootstrap Claude session screen automatically. If not, you'll see instructions to run step 2 above.
- Add a Forgejo connection (base URL + PAT with
write:repository write:issue read:user read:organization) - Create a workspace (Provider + Repo), then add one or more sessions from the workspace card
- Each session shows up at claude.ai/code and in the Claude Android/iOS app a few seconds later
API tokens
To read Nexus state from another machine — a status widget, a phone shortcut, a cron job — mint a scoped token under Settings → API tokens rather than handing the script your master passphrase. The passphrase unwraps the vault's data key; a token carries no key material at all and never unlocks the vault.
curl -H "Authorization: Bearer nxs_…" http://your-nexus:3001/api/quota
Two scopes, granted independently:
| Scope | Reaches |
|---|---|
quota:read |
/api/quota |
workspaces:read |
/api/workers, and a workspace's sessions and session health |
Tokens are read-only: they are rejected on any method other than GET, and on every route not explicitly listed above — including container logs, agent instructions, artifact contents, and token management itself. A token never unlocks the vault, so it cannot spawn a workspace or read a provider credential even if a route would otherwise allow it.
An expiry is optional and defaults to never. The plaintext is shown once at creation and only a SHA-256 digest is stored, so a lost token is replaced rather than recovered. Revoking takes effect immediately.
The token is a bearer credential in cleartext. Over plain HTTP anything on the network path can read it — put Nexus behind TLS before using tokens across an untrusted network. There is no rate limiting on the API yet.
Architecture
┌────────────────────────────────┐
│ browser / Claude mobile app │
└──────────────┬─────────────────┘
│ HTTPS (nginx, optional)
┌──────────────▼─────────────────┐ ┌─────────────────────────────┐
│agent-nexus (SvelteKit + SQLite)│ via TCP │ docker-socket-proxy │
│ - master-passphrase vault │────────►│ endpoint allowlist: │
│ - provider abstraction │ to:2375 │ containers, exec, volumes, │
│ - spawns workers │ │ images. Everything else 403.│
└──────────────┬─────────────────┘ └────────────┬─────────────────┘
│ creates / inspects │ mounts /var/run/docker.sock:ro
│ ▼
│ ┌─────────────────────────────┐
│ │ host Docker engine │
▼ └────────────┬─────────────────┘
┌───────────────────────────────────┐ │ spawns
│ agent-nexus-worker (one/workspace)│◄─────────────────┘
│ - tmux PID 1, N session windows │
│ - claude --remote-control NAME │ ┌─────────────────────────┐
│ cwd = .nexus/worktrees/NAME │──────►│ Anthropic cloud relay │
│ - /session ← claude-session vol │ └────────────┬────────────┘
│ - /workspace ← per-worker volume │ │
└───────────────────────────────────┘ │
▼
your Claude mobile / claude.ai/code
- One worker container = one tmux server = N sessions. Each session is a tmux window running its own
claude --remote-control, rooted in a Nexus-created git worktree at/workspace/.nexus/worktrees/<name>on a branch named exactly<name>, so collaborating agents share one/workspaceclone while their changes land on isolated branches. Nexus creates and removes those worktrees itself — it does not pass--worktreeto the CLI. - Shared OAuth state: the
claude-sessiondocker volume holdscredentials.json+account.jsonproduced byclaude auth login. Workers mount it read-write so refresh-token rotations persist across the fleet. - Docker socket is fronted by a proxy (
tecnativa/docker-socket-proxy) on a default-deny internal network. Even if Nexus is compromised, the attacker cannot reachdocker run --privilegedor any non-allowlisted endpoint. - Per-spawn secrets (Forgejo token, authed clone URL) are injected via tmpfs after the worker starts. They do NOT appear in
docker inspector/proc/1/environ. - Worker discovery: containers carry
nexus-managed=true. The UI never lists unrelated host containers.
Repo layout
agent-nexus/
├── nexus/ # the webapp (SvelteKit + Vite + Kysely)
├── worker/ # the Claude runtime image
├── docker-compose.yml # nexus + docker-socket-proxy
├── DEPLOYMENT.md # threat model, nginx, backups
└── .forgejo/workflows/ci.yml
See DEPLOYMENT.md for production deployment, nginx reverse-proxy config, the threat model after the socket-proxy hardening, and backup snippets.