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
# every empty value is documented inline; secrets: openssl rand -hex 32
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
On your mac run claude setup-token and put the result into .env as
CLAUDE_CODE_OAUTH_TOKEN. That is the whole auth: no /login inside the
container, no dialogs. The token draws from your subscription limits like
regular Claude Code; rotate it by running claude setup-token again.
Claude agents run on the Claude Agent SDK. Prompts are granules under
мета/бобер/промпты/ in the vault - plain markdown, the <tag> wrappers
and the order live in config.py; skills are the folders under
мета/бобер/скиллы/ (each becomes a plugin, keep name: in SKILL.md
latin). The vault must be synced before the gateway can start. The model process runs as beaver-runner
with a whitelisted environment; the vault is read-only for it except
мета/бобер and 💬 чаты.
4. Mint a token
Open admin at http://localhost:62990/ (or https://<DOMAIN>/ 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/anthropic/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
Everything is on one port under its own path; Caddy forwards the whole domain as is.
- any Anthropic client:
http://localhost:62990/anthropicorhttps://<DOMAIN>/anthropic, model = agent name (beaver-opus-highetc) - MCP clients (Claude Desktop, Raycast extension):
/mcp/<name>/, discovery page at/mcp/ - Obsidian companion plugin:
/mdas the plugin's "Base URL",/apifor the panel - Admin UI:
http://localhost:62990/orhttps://<DOMAIN>/, login fromADMIN_USER/ADMIN_PASS
Exposing to the internet
caddy/site.caddy holds the routes (one reverse_proxy, domain from BEAVER_DOMAIN); on a shared server it is bind-mounted into that server's Caddy as projects.d/beaver-agent/, on its own box caddy/docker-compose.yml runs a Caddy around it.
cd caddy
cp Caddyfile.example Caddyfile
cp .env.example .env # BEAVER_DOMAIN, BEAVER_UPSTREAM, Cloudflare token if any
docker compose up -d
Set PUBLIC_BASE_URL=https://<DOMAIN> in beaver-agent's .env so advertised endpoints use the outside origin.
Editing config.py on the mac
make sync installs ../beaver-gateway editable (extra local), so
imports in config.py resolve against the checkout you are working on;
uv sync --extra prod pulls the gateway from git instead. Neither is used
by the image - the container builds the gateway itself.
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 theclaude[<agent>]:stderr lines. Usually auth: redo step 3. - gateway restarts in a loop right after first
up-config.pyreads prompt granules from/vault/мета/бобер/промпты; until Obsidian Sync has pulled the vault they are missing. Finish step 2, it settles. - the model cannot write into
мета/бобер- the entrypoint grantsbeaver-runneran ACL on the two rw sub-mounts once at start; files that Sync creates later inherit it through the default ACL. Ifsetfaclis unsupported on the volume it falls back tochown, and then files Sync writes afterwards as root stay read-only for the model until the next restart.