`transport="stream_json"` puts them on `claude -p` with piped stdin instead of a pseudo-tty. The pi is the machine the PTY path hurt most: seconds per spawn waiting for an Ink render loop to settle — which is why `startup_delay` had crept to 60s — and the occasional prompt lost to a swallowed paste. Neither failure mode has an analogue on a pipe. `include_partial_messages` comes with it, so Anthropic-API clients get token-level SSE rather than one delta per finished block. Drops `--remote-control`: it only means anything for an interactive session, and the transport would strip it anyway.
beaver-agent
My real beaver-gateway setup (paired with the protocol from beaver.kotikot.com).
Uses Claude Code and Raycast subscriptions for agents, has some MCPs set up. You can use this as-is or modify to match your needs (config.py).
Requirements
- Docker + Docker Compose
- Obsidian Sync (for
obsidian-headless) - claude.ai sub
- raycast sub
- Firefly III and its PAT
First launch
1. Create .env and raycast.json
cp .env.example .env
# fill CHANGE-ME
nvim .env
# build raycast config on your mac (beta by default, consider checking raycast-api for instructions)
raycast-api init
# copy config.json to your deployment server
docker compose up -d
2. Set up Obsidian Sync
docker exec -it beaver-obsidian ob login
docker exec -it beaver-obsidian ob sync-setup --vault "yourvault"
docker restart beaver-obsidian
Check:
docker exec beaver-obsidian ls /vault
Only markdown is synced by default. To sync everything:
docker exec beaver-obsidian ob sync-config \
--file-types image,audio,video,pdf,unsupported
docker restart beaver-obsidian
3. Set up claude
docker exec -it beaver-gateway claude /login
docker exec -it beaver-gateway bash -c 'cd /vault && claude --dangerously-skip-permissions --model claude-opus-4-7'
Click through every dialog, then /exit or Ctrl+C.
4. Mint a token
Open admin at http://localhost:62992 (or https://<DOMAIN>/admin/ if Caddy), sign in with ADMIN_USER / ADMIN_PASS, go to Tokens → Create, scope * for first run.
5. Smoke test
docker exec beaver-gateway ls /vault | head
docker logs beaver-gateway | grep -i "agent registered"
curl http://localhost:62990/v1/models \
-H "Authorization: Bearer <YOUR_TOKEN>"
Where to plug things in
The admin dashboard renders ready-to-copy URLs and snippets for each of these
- any Anthropic client:
http://localhost:62990orhttps://<DOMAIN>/anthropic, model = agent name (beaver-opus-highetc) - MCP clients (Claude Desktop, Raycast extension): just find in admin
- Obsidian companion plugin: paste the dashboard's plugin base (
http://localhost:62993raw, orhttps://<DOMAIN>/mdbehind Caddy) into the plugin's "Base URL" setting - Admin UI:
http://localhost:62992orhttps://<DOMAIN>/admin, login fromADMIN_USER/ADMIN_PASS
Exposing to the internet
Reference config in caddy. Designed for: gateway on rpi, Caddy on a server with a public IP, wired through tailscale.
cd caddy
cp Caddyfile.example Caddyfile
cp .env.example .env # only for cloudflare
# replace <DOMAIN>; point upstreams at wherever the gateway is reachable
docker compose up -d
Set PUBLIC_BASE_URL=https://<DOMAIN> in beaver-agent's .env. For per-subdomain layout, rewrite the Caddyfile and pass full public_base_url=... per frontend in config.py.
Useful commands
# shell into the gateway container (claude CLI, bun, python with venv all live here)
docker exec -it beaver-gateway bash
# what obsidian-sync has pulled
docker exec beaver-obsidian ob status
# force a one-off sync (don't wait for the continuous tick)
docker exec beaver-obsidian ob sync
# per-service logs
docker compose logs -f gateway
docker compose logs -f obsidian-headless
# pull the latest beaver-gateway (when GATEWAY_REF=main)
docker compose build gateway && docker compose up -d gateway
# reload config.py without a full rebuild
docker compose restart gateway
Gotchas
- claude in the container doesn't see the vault -
cwd=VAULTinconfig.pyresolves to/vaultinside the container, not on the host. Don't change it. - a claude turn fails immediately - check
docker logs beaver-gatewayfor the claude stderr it now quotes back. Usually auth: re-run step 3'sclaude /login. - the admin terminal viewer is read-only - claude agents run headless (
transport="stream_json"inconfig.py), so what the viewer shows is the JSON event stream, not a TUI you can type into. Flip that agent totransport="pty"if you genuinely need a keyboard on a live session.
The claude agents used to hang on JSONL file did not appear within 30s when the TUI's onboarding hadn't been clicked through. That path is gone with the headless transport — prompts go into a pipe, not a terminal.