Deployment
Before deploying, read the security & threat model. Off loopback the relay requires a TLS-terminating reverse proxy; agentic mode with broad permissions must be containerized; and a shared caller token is a shared identity on your subscription and host.
Terms of service: relayed requests run under your Anthropic account's terms. Consumer subscriptions are personal — deploy the relay for your own use on a private network only, and never share or resell access to your account through it. See the disclaimer.
The runtime image is not just the Go binary: the Claude backend spawns
the claude CLI, so the image bundles the CLI (and its Node runtime), and
the host's subscription auth is provided via a mounted volume.
Docker
docker build -t agent-relay .
The provided Dockerfile is a two-stage build: a Go builder producing a
static relay binary, and a node-based runtime with
@anthropic-ai/claude-code installed globally.
First-time (headless) login, once per volume:
docker compose up -d
docker compose exec relay claude login
The /home/relay/.claude volume persists the login across restarts.
docker-compose
See docker-compose.yml. Adjust RELAY_BIND to your private interface
(e.g. the Tailscale IP) and set a strong token:
RELAY_TOKENS=$(openssl rand -hex 32) docker compose up -d
Remember the startup guard: a non-loopback bind without RELAY_TOKENS
refuses to start.
Inside a container, set RELAY_TOKENS regardless of RELAY_BIND
The startup guard reasons about the address the process binds. It cannot
see the published port: docker run -p 18082:18082 exposes the relay on
the host's 0.0.0.0 even though the process bound 127.0.0.1 inside the
container and therefore required no token. Publish to a specific
interface and always set a token — the shipped docker-compose.yml does
both (127.0.0.1:18082:18082 and RELAY_TOKENS=${RELAY_TOKENS:?…}). The
guard protects the bind; the port mapping is yours to get right.
Reverse proxy (mandatory off loopback)
The relay speaks plain HTTP and has no per-caller rate limit by design. Off loopback, front it with a proxy that terminates TLS and throttles abuse — see the security & threat model.
With Caddy (automatic TLS, rate limiting via the caddy-ratelimit plugin):
relay.example.ts.net {
encode gzip
rate_limit {
zone relay {
key {http.request.header.x-api-key}
events 60
window 1m
}
}
reverse_proxy 127.0.0.1:18082 {
flush_interval -1 # required: do not buffer SSE streams
}
}
With Tailscale alone (no extra TLS setup — tailscale serve terminates TLS
with a Tailnet certificate):
tailscale serve --bg --https=443 http://127.0.0.1:18082
Whichever you choose: keep the relay bound to 127.0.0.1 and let the proxy
be the only thing listening on the network interface, and make sure streaming
responses are not buffered (flush_interval -1 in Caddy,
proxy_buffering off in nginx) or SSE will arrive in one chunk at the end.
NixOS
On NixOS hosts, build with buildGoModule and expose the relay as a systemd
service bound to tailscale0 — the fully declarative path:
{ buildGoModule }:
buildGoModule {
pname = "agent-relay";
version = "0.1.0";
src = ./.;
# The A2A adapter pulls in the official a2a-go SDK; everything else is stdlib.
vendorHash = "sha256-..."; # nix will tell you the right hash on first build
}
A minimal service unit sets the RELAY_* environment and runs the binary as
an unprivileged user with the claude CLI on PATH.
Operational notes
GET /healthis unauthenticated and suitable for container health checks.- One subprocess per in-flight request: size
RELAY_MAX_CONCURRENTto the host's memory/CPU budget; excess requests receive an immediate 503. - Structured JSON logs go to stderr; each request logs its
X-Request-Id.