Connect Claude Code to Paddock
At the end of this guide, a claude session on your laptop can list the
projects on your Paddock instance, list their chats, and read a transcript —
and cannot start a turn, send a message, or create anything.
That last part is the point. This is the deliberately boring, read-only version, and it’s the one you should set up first.
What you need
Section titled “What you need”- Paddock v0.46.0 or newer. The
/mcpendpoint and the authenticator that gates it shipped in 0.46.0. On anything older this doesn’t exist. - A way to edit
paddock.config.yamlon the instance, and to set an environment variable for the Paddock process. - Claude Code on your laptop — the
claudeCLI. - A way to reach the instance over TLS, or a way to reach it over loopback. Read the next section before you start; it’s the one decision that changes the rest.
You do not need a secrets manager, a reverse proxy, an identity provider, or a container orchestrator. None of them appear below.
First, decide how you’ll reach it
Section titled “First, decide how you’ll reach it”Paddock refuses a plaintext /mcp request from a non-loopback client with
403 insecure_transport. The endpoint carries a bearer token in a header, so
this is deliberate — and it’s the step people hit last and debug longest. Settle
it first.
Three routes, simplest first. Pick one and remember which; step 3 and step 5 both depend on it.
Route A — over an SSH tunnel (no proxy at all)
Section titled “Route A — over an SSH tunnel (no proxy at all)”If you already have SSH to the box, this is the least moving parts of anything
in this guide, and it is genuinely secure: SSH encrypts the hop, and Paddock
sees a loopback client because sshd connects to the port from the host itself.
# on your laptop, in a terminal you leave openssh -N -L 4000:127.0.0.1:4000 you@your-paddock-hostYour instance is now at http://127.0.0.1:4000 from the laptop, and
http://127.0.0.1:4000/mcp is a legitimate plaintext URL — nothing crosses the
network in the clear.
The trade-off is that the tunnel has to be up for the connection to work; a
claude session started without it will just fail to connect.
Route B — a real hostname with automatic HTTPS
Section titled “Route B — a real hostname with automatic HTTPS”For a permanent connection you want real TLS. The
auth-basic/caddy
recipe in paddock-deploy is the shortest path to it: point a DNS name at the
box, set SITE_ADDRESS to that name, and Caddy provisions a Let’s Encrypt
certificate on its own. The recipe already exempts /mcp and
/.well-known/oauth-protected-resource* from its Basic Auth challenge, which
you will need — see Route C.
Any TLS-terminating proxy works. Caddy is suggested because it needs the least configuration to get a valid certificate.
Route C — you already run a proxy or an edge login
Section titled “Route C — you already run a proxy or an edge login”Then you have one extra job, and MCP cannot work until it’s done: exempt
/mcp and /.well-known/oauth-protected-resource* from your edge auth.
Basic Auth collides with MCP head-on — the proxy challenges with the
Authorization header, and the MCP client puts its own Authorization: Bearer …
in that same header, so there is no password that is also a valid MCP
credential. An SSO proxy fails differently and just as fatally: it answers with
a 302 to an HTML login page, which no MCP client can follow.
Exempting is safe because Paddock authenticates /mcp itself, independently of
PADDOCK_AUTH_MODE and of your proxy. Don’t duplicate the recipes here — see
Securing Paddock → The /mcp Management API
for the rules (including a deploy-ordering hazard you should read before
touching a proxy config), and paddock-deploy’s
auth-basic/
and kubernetes/
recipes for configurations that carry the exemption already.
1. Mint a token
Section titled “1. Mint a token”On any machine with openssl:
printf 'pdk_%s_%s\n' myinstance "$(openssl rand -hex 24)"Replace myinstance with a short name for this instance; you’ll repeat it as
instanceId in step 3. The pdk_<instanceId>_ prefix binds the token to this
instance — copy it to a second Paddock and it is refused there even though the
bytes are identical — and gives secret scanners something to match on. An
unprefixed token still authenticates, but logs a warning that it isn’t bound.
Tokens shorter than 24 characters are dropped with a warning, measured across
the whole string including the prefix. openssl rand -hex 24 gives you 48
characters of secret on its own, so you’re well clear.
Keep the output somewhere you can paste from twice — into the instance’s
environment in step 2, and into claude mcp add in step 5. It is never
recoverable from Paddock afterwards.
2. Put the token in the instance’s environment
Section titled “2. Put the token in the instance’s environment”Not in the YAML. paddock.config.yaml is git-tracked and editable from the
instance Config screen, so a literal token: or secret: in it is a hard
config error — Paddock rejects the client rather than accepting a secret
you’re about to commit. The config only ever holds the name of an environment
variable.
Set PADDOCK_MCP_TOKEN_LAPTOP wherever your setup passes environment to the
Paddock process:
PADDOCK_MCP_TOKEN_LAPTOP=pdk_myinstance_1a2b3c…docker run -e PADDOCK_MCP_TOKEN_LAPTOP=pdk_myinstance_1a2b3c… …services: paddock: environment: PADDOCK_MCP_TOKEN_LAPTOP: "${PADDOCK_MCP_TOKEN_LAPTOP:-}"The variable name is entirely yours — it just has to match the ref: in step 3.
One variable per client. paddock-deploy’s
docker/ recipe
ships with exactly this line already wired through from .env.
3. Add the managementApi block
Section titled “3. Add the managementApi block”Six lines in paddock.config.yaml:
managementApi: instanceId: myinstance publicUrl: https://paddock.example.com clients: laptop: auth: ref: env:PADDOCK_MCP_TOKEN_LAPTOP # No `scope:` block — that is what makes this client read-only.Three things to get right:
-
publicUrlis mandatory as soon asclientsis set. Omit it, or write something that fails validation, and Paddock discards every client — so/mcpstays 404. That 404 is the single most common reason this doesn’t work, and the config error naming the field is in the log.Paddock deliberately refuses to derive this from the
Hostheader: that header is attacker-controlled, and behind a TLS-terminating proxy the derived scheme would be wrong anyway. So state the real origin. A merely inaccurate value that still validates won’t break a bearer-token client — it’s only used to build the OAuth discovery document, which isn’t published here — but it is a typo that stays silently wrong, so get it right while you’re looking at it.- On Route A (SSH tunnel) that value is
http://127.0.0.1:4000— plainhttpis accepted forlocalhost,127.0.0.1and::1, and only for those. Any other host must behttpsor the management API refuses to start. - On Routes B and C it’s your real external origin,
https://paddock.example.com. No trailing slash, no query string, no fragment.
- On Route A (SSH tunnel) that value is
-
instanceIdmust match the one you baked into the token in step 1, or the token is refused. -
laptopis the client id. It’s yours to choose; it’s what gets logged and stamped as provenance. The token itself never is.
Leave authorizationServers out. It only advertises OAuth issuers, and OAuth
isn’t shipped — see What this doesn’t cover.
4. Restart, and read the log line
Section titled “4. Restart, and read the log line”Restart Paddock and look for one of these two lines:
management API: /mcp enabled (self-authenticated — independent of PADDOCK_AUTH_MODE and of any proxy)It carries the enabled client ids and the instanceId as structured fields.
That’s success. Otherwise:
management API: /mcp disabled (no managementApi.clients configured) — the endpoint 404sEvery rejection is logged with the reason, at error for something malformed
(an inline secret, an unknown auth.type, a missing or non-env: ref, a bad
publicUrl) and at warn for a client whose credential simply wouldn’t resolve
— env var unset, blank, or under 24 characters. Read the message; it names the
field.
A misconfigured management API never takes the instance down. It fails closed and the rest of Paddock carries on, which is why you have to actually look at the log rather than assume a clean boot means a working endpoint.
5. Add the server to Claude Code
Section titled “5. Add the server to Claude Code”On your laptop:
claude mcp add --transport http --scope user paddock \ https://paddock.example.com/mcp \ --header "Authorization: Bearer pdk_myinstance_1a2b3c…"On Route A, the URL is http://127.0.0.1:4000/mcp instead, and the tunnel from
step 0 has to be up.
Three details that each cost somebody an afternoon:
- Use
--scope useror--scope local. A--scope projectserver needs interactive approval before it will connect. --transport httpis not optional. An entry with aurlbut no type is treated as a stdio server and fails. The CLI writes"type": "http"for you; hand-edited JSON must include it.- A configured
Authorizationheader and OAuth are mutually exclusive. If the header is rejected, the client reports a failed connection — it does not fall back to anything.
The token lands in your shell history here. Clear it, or pass it via a variable you’ve already exported, if that matters to you.
6. Verify
Section titled “6. Verify”Start claude and check the tools it now has. You should see exactly four:
list_projects list_chats read_chat list_triggersand you should not see create_chat or send_message. Out-of-scope tools
are hidden rather than offered-and-refused, so their absence is the confirmation
that the read-only default took effect. If you see write verbs, you have a
scope block you didn’t mean to write.
To check the endpoint without a client at all:
curl -sS -X POST https://paddock.example.com/mcp \ -H "Authorization: Bearer $PADDOCK_MCP_TOKEN_LAPTOP" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'Two things about that command surprise people:
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause |
|---|---|
404 {"error":"not found"} | The management API is off. No clients, or no publicUrl, or publicUrl failed validation — the log line from step 4 says which. |
403 insecure_transport | Plaintext from a non-loopback client. Use TLS (Route B/C) or a tunnel (Route A). Do not add X-Forwarded-Proto. |
401 + WWW-Authenticate: Bearer | Token missing, malformed, or matching no client. Check the instanceId in the token matches the config, and that the env var actually reached the process. |
406 | No Accept: application/json, text/event-stream. Not an auth problem. |
405 + Allow: POST | You sent GET or DELETE with a valid token. /mcp is POST-only; it refuses rather than hanging on a stream that never emits. Without a token the same request is a 401 — the auth gate runs first — so opening /mcp in a browser tells you nothing useful. |
302 to a login page | Your edge proxy is gating /mcp. Exempt it — Route C. |
401 from a Basic Auth realm | Same thing: the proxy consumed the Authorization header before Paddock saw it. |
| Connects, but no tools | The client’s credential resolved but every operation is out of scope. A scope block with an empty allow grants nothing. |
| Connects, but write tools appear | You have a scope block granting them. Re-read the warning at the top of this page. |
403 from inside a container, over loopback | A published port is not loopback from inside — Docker NATs the peer address. Test from the host instead. |
What this doesn’t cover
Section titled “What this doesn’t cover”- OAuth. It isn’t implemented
(#473). Static bearer tokens
are the only credential that works today. Paddock publishes an RFC 9728
discovery document only when you configure
authorizationServers, and deliberately publishes nothing otherwise rather than emit a document the MCP spec would call invalid — a client holding a static token never performs discovery, so nothing is lost. - Write access. Covered, with its full risk framing, in the Management API reference.
- Per-project scoping,
denyrules andmaxSpawnDepth— same place.
See also
Section titled “See also”- Management API (MCP) — the complete reference: response matrix, scope semantics, discovery document, token binding.
- Securing Paddock — the edge exemption, and the deploy-ordering hazard.
- Self-management MCP — the same tools as a keeper sees them from inside an instance.
paddock-deploy— Docker, Proxmox, Kubernetes and Basic-Auth recipes, all carrying the/mcpexemption.