This guide runs CLIProxyAPI in an Amp orb around the clock and publishes it at a stable public HTTPS URL through Tailscale Funnel. An Amp Custom URL model-routing connection then sends Claude requests there, and CLIProxyAPI serves them with your Claude Pro or Max subscription (OAuth login) instead of Amp credits.
The host scripts, templates, and an agent skill that walks you through every step live in ben-vargas/amp-plugins/skills/amp-cli-proxy-api. The easiest path is to install the skill and ask Amp to set it up:
amp skill add --global ben-vargas/amp-plugins/skills/amp-cli-proxy-api
Then, in an Amp thread: "Use the amp-cli-proxy-api skill to set up CLIProxyAPI." The rest of this guide is the same procedure by hand.
Amp servers
│ POST https://<hostname>.<tailnet>.ts.net/v1/messages
│ Authorization: Bearer <CLIPROXY_API_KEY>
▼
Tailscale Funnel relay (public DNS, TLS passthrough)
▼
Amp orb, Tailscale node "<hostname>" (tag:amp-cli-proxy-api, ephemeral)
│ tailscaled terminates TLS, forwards to 127.0.0.1:8317
▼
cli-proxy-api (supervised orb service "cliproxy")
│ checks the API key, picks the Claude OAuth credential
▼
api.anthropic.com (your Claude subscription)
Placeholders used below: <tailnet> (your *.ts.net tailnet name), <hostname> (default amp-cli-proxy-api), <amp-user-id>, <project-id>, <workspace-id>, <client-id>, and <namespace>/<project> for your Amp project.
Using a subscription through a proxy is your decision under Anthropic's terms.
-
Amp calls Custom URL connections from its own servers , so the endpoint must be publicly reachable over HTTPS. Orb ports are not public, and orb portals need an Amp sign-in or a "Make Public" that expires.
-
Tailscale Funnel gives a stable
*.ts.netname, a real TLS certificate, and streaming (SSE). Cloudflare Quick Tunnels were rejected: no SSE and a new URL on every run. -
Orbs when idle , and a running service does not keep one awake. A small Amp plugin holds a keep-alive lease, but only on the one orb marked as the host.
-
The orb joins the tailnet with Amp workload identity (OIDC) , so no Tailscale auth key is stored anywhere. The node is ephemeral.
-
Only the host orb joins the tailnet. Every orb of the project installs Tailscale, but
tailscaledstays stopped until.agents/cliproxy-host enableruns in that orb. -
An Amp account with orbs and Custom URL model routing (Amp Settings → Model Routing → Add should offer Custom URL).
-
A Tailscale account where you are Owner or Admin (the free plan works), with MagicDNS on andHTTPS Certificates enabled (admin console → DNS).
-
A Claude Pro or Max subscription.
-
Budget: the host orb never s. On
a1.small(2 CPUs, 4 GB) that is about $0.17/hour, roughly $124/month. Observed load while serving Claude Opus: load average about 0.07 and about 700 MB of memory in use.
Create a new, dedicated Amp project (an Amp-hosted repository is fine) and set its orb size to a1.small . These files own.agents/setup ,.agents/resume , and.amp/services.yaml , so a dedicated repository avoids clashing with another project's setup.
2.
Clone the project's repository and install the host files into it with the skill's scaffold script:
amp skill add --global ben-vargas/amp-plugins/skills/amp-cli-proxy-api
bash ~/.config/agents/skills/amp-cli-proxy-api/scripts/scaffold.sh <repo-dir>
It writes the files below, copies the skill into .agents/skills/amp-cli-proxy-api/ , and adds.gitignore entries. That skill copy is ignored by Git:.agents/setup reinstalls the published skill in every orb (.agents/cliproxy-host skill ), so you never maintain a copy in your project. If it exits 3, some of those files already exist with other content and nothing was changed; merge by hand, or rerun with--force to replace them.
3.
Commit and push to the default branch. Orbs only use files on the default branch.
What each file does:
| Path | Purpose |
|---|---|
.agents/setup |
Runs for new orbs and project snapshots. Checks out CLIProxyAPI into src/ , installs the Go version from itsgo.mod , buildsbin/cli-proxy-api , installs the Claude Code CLI, installs Tailscale and leavestailscaled disabled. Idempotent. Never authenticates anything, because its output is shared through snapshots. |
.agents/cliproxy-host |
Host control script: fetch ,build ,update ,serve ,enable ,resume ,status ,disable . |
.agents/resume |
Runs after every orb activation and wake; calls cliproxy-host resume , which does nothing unless this orb is the host. |
.amp/services.yaml |
Declares the supervised cliproxy service on port 8317 with a/healthz check. |
.amp/plugins/cliproxy-host-keepalive.ts |
Every 60 s checks for ~/.config/cliproxy-host/enabled ; while it exists, holdsamp.system.executor.keepAlive() . |
.gitignore |
Keeps the source checkout, binary, config.yaml , and runtime state out of Git. |
Run amp projects get <namespace>/<project> to get the project ID and owner. The subject Tailscale must match is:
- personal project:
user:<amp-user-id>:project:<project-id> - workspace project:
workspace:<workspace-id>:project:<project-id>
When in doubt, run amp orb id-token --audience test --subject-scope project in an orb of the project and decode the token's sub claim. Do not paste the token anywhere.
All pages are in the admin console at https://login.tailscale.com/admin.
Create the tag. Access controls → Definitions → Tags → Create tag: nameamp-cli-proxy-api , ownerautogroup:admin . The trust credential can only use a tag that already exists.
2.
Create the OIDC trust credential. Settings → Trust credentials → Credential → OpenID Connect:Field Value Issuer Custom issuer Issuer URL https://ampcode.com/api/workload-identity Subject the subject from step 2 Audience leave empty; Tailscale generates one Custom claims project_id =<project-id> andtoken_use =exchanged`` token_use must be exactlyexchanged ; a typo such asexchange makes every join fail with HTTP 403. On the Scopes page keep Custom scopes, check onlyKeys → Auth Keys → Write , andselecttag:amp-cli-proxy-api from the Tags dropdown so it shows as a chip (typed text leaves Generate disabled). Copy the** Client ID** andAudience (api.tailscale.com/<client-id> ). Neither is a secret.
3.
Allow Funnel for the tag. Access controls → Definitions → Node attributes → Add: targettag:amp-cli-proxy-api , attributefunnel . The defaultautogroup:member funnel row does not cover tagged machines.
4.
Allow SSH (optional). In the policy JSON, add a rule to thessh list. The defaultautogroup:self rule does not cover tagged machines.user is the orb's login user, which has passwordless sudo.
{
"src": ["autogroup:member"],
"dst": ["tag:amp-cli-proxy-api"],
"users": ["user"],
"action": "accept",
}
Policy-file equivalent of the tag, Funnel, and SSH steps:
"tagOwners": { "tag:amp-cli-proxy-api": ["autogroup:admin"] },
"nodeAttrs": [{ "target": ["tag:amp-cli-proxy-api"], "attr": ["funnel"] }],
"ssh": [{ "src": ["autogroup:member"], "dst": ["tag:amp-cli-proxy-api"], "users": ["user"], "action": "accept" }]
In the Amp project's Secrets & Env Vars settings:
| Name | Kind | Value |
|---|---|---|
CLIPROXY_API_KEY |
Secret | A random key Amp will send. Generate one with echo "sk-$(openssl rand -hex 20)" and keep it for step 8. |
TS_CLIENT_ID |
Env var | Trust credential Client ID. |
TS_AUDIENCE |
Env var | Trust credential Audience. |
CLIPROXY_REPO |
Env var, optional | Git URL to build from. Default https://github.com/router-for-me/CLIProxyAPI . Set it to your fork if you have one. It must be clonable without credentials. |
CLIPROXY_REF |
Env var, optional | Branch or tag. Default main . |
CLIPROXY_TS_HOSTNAME |
Env var, optional | Tailscale hostname. Default amp-cli-proxy-api . |
CLIPROXY_TS_TAG |
Env var, optional | Tailscale tag. Default tag:amp-cli-proxy-api . |
CLIPROXY_SKILL_REPO /CLIPROXY_SKILL_REF |
Env vars, optional | Where setup installs the skill from, if you fork it. Default https://github.com/ben-vargas/amp-plugins ,main . |
Set CLIPROXY_REPO/ CLIPROXY_REF before the first orb starts. A running orb only sees changed values after amp orb restart-processes, and a changed source also needs .agents/setup rerun.
Start a thread in the project with an orb. Amp runs .agents/setup (about 2–3 minutes cold, mostly the Go build). Then, in the orb (agent, Terminal tab, or SSH):
cd ~/workspace/repo
for v in TS_CLIENT_ID TS_AUDIENCE CLIPROXY_API_KEY; do [ -n "${!v:-}" ] && echo "$v set" || echo "$v missing"; done
.agents/cliproxy-host enable
Success ends with:
Custom URL base: https://<hostname>.<tailnet>.ts.net/v1
enabled: this orb is the CLIProxyAPI host and will be kept awake
enable builds if needed, writes config.yaml (mode 600, api-keys from CLIPROXY_API_KEY, debug: true), joins the tailnet with an exchanged OIDC token, turns on Funnel for port 8317, starts the service, and creates the host marker the keep-alive plugin watches.
If the URL has a -1 suffix (<hostname>-1.<tailnet>.ts.net), an older node still holds the name; see Replacing the host.
In the orb's Terminal tab:
cd ~/workspace/repo && bin/cli-proxy-api --claude-login --no-browser
- Open the printed
https://claude.ai/oauth/authorize…URL in your browser and approve. - The browser then fails to load
http://localhost:54545/callback?code=…, because that port is inside the orb. Copy that full URL from the address bar. - Wait for the
Paste the Claude callback URLprompt. It appears about15 seconds afterWaiting for Claude authentication callback..., so the command looks stuck until then. Paste the URL and press Enter.
Ignore the SSH tunnel instructions the command prints. Do not press Ctrl-Z; a suspended process cannot finish the login. If you did, run fg to resume it.
The running proxy picks up the new credential from ~/.cli-proxy-api/ without a restart. Check it:
ls ~/.cli-proxy-api/claude-*.json
curl -s -H "Authorization: Bearer $CLIPROXY_API_KEY" localhost:8317/v1/models | jq '.data | length' # > 0
New Funnel names can take about 10 minutes to appear in public DNS. Check with public resolvers, not the ts.net authoritative servers:
H=<hostname>.<tailnet>.ts.net
for r in 8.8.8.8 1.1.1.1; do dig +short @$r $H A; done
curl -s https://$H/healthz # {"status":"ok"}
curl -s -o /dev/null -w '%{http_code}\n' https://$H/v1/models # 401 without a key
curl -s -H "Authorization: Bearer $CLIPROXY_API_KEY" https://$H/v1/models | jq '.data | length'
curl -sN -H "Authorization: Bearer $CLIPROXY_API_KEY" -H 'content-type: application/json' \
https://$H/v1/messages \
-d '{"model":"claude-haiku-4-5-20251001","max_tokens":20,"stream":true,"messages":[{"role":"user","content":"Reply with just: ok"}]}'
The stream must show message_start, a content_block_delta with "ok", and message_stop.
Right after enable, the Funnel relay can take a minute or two to pick up the name and certificate; TLS handshakes fail until then.
Amp Settings → Model Routing → Add → Custom URL. A personal connection applies to your threads in every workspace; a workspace connection applies to all members.
Base URL: https://<hostname>.<tailnet>.ts.net/v1 (requests go to…/v1/messages ) #
API key: your CLIPROXY_API_KEY #
API format: compatible with the Anthropic Messages API #
Models, one per line (Amp model → CLIProxyAPI model; list the proxy's IDs with /v1/models ):
anthropic/claude-fable-5-1 -> claude-fable-5-1
anthropic/claude-opus-5-5 -> claude-opus-5-5
anthropic/claude-haiku-4-5-20251001 -> claude-haiku-4-5-20251001
Make sure the connection is active (new connections can start inactive) and ordered ahead of any other connection that maps the same models. Then confirm which connection serves:
amp config model-providers check-access --provider-model anthropic/claude-opus-5-5
The result should name your Custom URL connection (connectionType: model_provider_custom_url). In the host orb, each routed request logs a line like Use OAuth provider=claude auth_file=… for model claude-opus-5-5.
Run in the host orb from ~/workspace/repo:
| Task | Command |
|---|---|
| Status | .agents/cliproxy-host status |
| Deploy the latest source | .agents/cliproxy-host update |
| Follow logs | sudo journalctl -u amp-svc-cliproxy -f |
| Recent logs | amp orb service logs cliproxy -n 200 |
| Routed requests and models | sudo journalctl -u amp-svc-cliproxy | grep 'Use OAuth' |
| Restart the proxy | amp orb service restart cliproxy |
| SSH from a tailnet device | tailscale ssh user@<hostname> |
| Stop being the host | .agents/cliproxy-host disable |
The proxy writes no log files; output goes to the systemd journal unit amp-svc-cliproxy. In-place edits to config.yaml are hot-reloaded, but an edit that replaces the file (such as sed -i) may not be noticed; restart the service then.
Archiving the host thread s the orb and takes the proxy offline. Routed threads lose their model until you deactivate the connection or bring a host back.
Only one node can hold the hostname, and routed threads lose their model while the proxy is down, including a thread doing the replacement. Order:
- Deactivate the Custom URL connection in Amp Model Routing so threads fall back to Amp.
- Start a new orb thread in the project and let setup finish.
- On the old host, run
.agents/cliproxy-host disable, preferably from its Amp Terminal tab. It logs out of Tailscale, which should delete the ephemeral node and free the name. Over Tailscale SSH the session drops as the node leaves, but the teardown still finishes. - Check the Tailscale Machines page. The old node can linger even after
disable; if it is still listed, remove it. - On the new orb, run
.agents/cliproxy-host enable, then log in to Claude (step 6). - When
https://<hostname>.<tailnet>.ts.net/healthzanswers from outside, reactivate the connection. The URL and key are unchanged.
If the new node still came up as <hostname>-1, remove the old machine on the Machines page, then run:
.agents/cliproxy-host reclaim
It renames the node through a temporary name back to <hostname> (a plain tailscale set --hostname can leave it on -1) and resets Funnel so only the exact name is served. Public requests can fail for a few minutes while DNS and the relay catch up.
Moving to another Amp project changes the project ID: update the trust credential's subject and project_id claim, and copy the variables and secret to the new project.
| Symptom | Cause and fix |
|---|---|
enable :token exchange failed with status 403 |
Trust credential mismatch. The credential's page in the Tailscale console shows which claim failed. Check the issuer URL, subject (personal vs workspace format, project ID), project_id claim, and thattoken_use is exactlyexchanged . |
enable :TS_CLIENT_ID is not set |
Add the variable to the project, then amp orb restart-processes . |
enable :tailscale funnel failed |
No funnel node attribute for the tag, or HTTPS certificates are off for the tailnet. |
tailscaled stuck inNeedsLogin without an error |
The systemd drop-in from .agents/setup is missing (E2B orbs expose only a link-local interface); rerun setup. |
Node named <hostname>-1 |
An older node holds the name. Remove it on the Machines page, then run .agents/cliproxy-host reclaim . |
| Public name does not resolve | New Funnel DNS can take about 10 minutes. Check with dig @8.8.8.8 . |
Public TLS handshake fails right after enable or a rename |
The relay has not picked up the name or certificate yet. sudo journalctl -u tailscaled | grep -E 'cert|ingress' shows progress; wait a minute or two. |
/v1/models 401 with the right key |
The proxy uses an old config.yaml . Rerunenable (restarts on key change) or restart the service. |
/v1/models returns an empty list |
No provider is logged in; do step 6. |
| Claude login shows no paste prompt | It appears about 15 seconds after Waiting for Claude authentication callback... . Wait. |
| Claude login never completes | The callback URL was not pasted, or the process was suspended with Ctrl-Z. fg resumes it; otherwise rerun. |
ssh: Could not resolve hostname |
That device does not use Tailscale DNS. Use tailscale ssh , the 100.x address, or enable "Use Tailscale DNS settings". |
| Orb d anyway | Host marker missing, credits exhausted, or the thread was archived. Check .agents/cliproxy-host status and look forcliproxy-host: keep-alive lease acquired in~/.cache/amp/logs/headless.log . |
| Amp threads still use Amp credits | The connection is inactive, another connection wins precedence, or the mapping lacks the model. Run check-access and compare the serving connection. |
- Only requests carrying
CLIPROXY_API_KEYreach a provider;/healthzand/are public. - The Tailscale node is tagged and ephemeral, and your tailnet policy decides who can reach or SSH into it. No Tailscale key is stored: each join exchanges a short-lived Amp OIDC token.
- OAuth logins happen only in the host orb and stay in
~/.cli-proxy-api/. Never copy them into the repository, a snapshot, or.agents/setup. - Never share
config.yaml,~/.cli-proxy-api/*.json,CLIPROXY_API_KEY, or ID tokens. The credential file name contains your account email. - Debug logging records request metadata such as model names; review logs before sharing.
- Anyone who can view the host thread can use its Terminal and see the orb's files.