# Amp Orbs Running CLIProxyAPI + Amp Router Custom URL

> Source: <https://gist.github.com/ben-vargas/a4fc7155b570e4030734f5bbadffa7d0>
> Published: 2026-09-28 07:51:00+00:00

This guide runs [CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI) in an [Amp orb](https://ampcode.com/docs/orbs) 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](https://github.com/ben-vargas/amp-plugins/tree/main/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.net` name, a real TLS certificate, and streaming (SSE). Cloudflare Quick Tunnels were rejected: no SSE and a new URL on every run.
- **Orbs pause 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`tailscaled` stays stopped until`.agents/cliproxy-host enable` runs 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 and**HTTPS Certificates** enabled (admin console → DNS).
- A Claude Pro or Max subscription.
- Budget: the host orb never pauses. 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.

1. 
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 its`go.mod` , builds`bin/cli-proxy-api` , installs the Claude Code CLI, installs Tailscale and leaves`tailscaled` 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, holds`amp.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](https://login.tailscale.com/admin).

1. 
**Create the tag.** Access controls → Definitions → Tags → Create tag: name`amp-cli-proxy-api` , owner`autogroup: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>` and`token_use` =`exchanged`` token_use` must be exactly`exchanged` ; a typo such as`exchange` makes every join fail with HTTP 403. On the Scopes page keep Custom scopes, check only**Keys → Auth Keys → Write** , and**select**`tag:amp-cli-proxy-api` from the Tags dropdown so it shows as a chip (typed text leaves Generate disabled). Copy the** Client ID** and**Audience** (`api.tailscale.com/<client-id>` ). Neither is a secret.
3. 
**Allow Funnel for the tag.** Access controls → Definitions → Node attributes → Add: target`tag:amp-cli-proxy-api` , attribute`funnel` . The default`autogroup:member` funnel row does not cover tagged machines.
4. 
**Allow SSH (optional).** In the policy JSON, add a rule to the`ssh` list. The default`autogroup: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](#replacing-or-moving-the-host).

In the orb's Terminal tab:

```
cd ~/workspace/repo && bin/cli-proxy-api --claude-login --no-browser
```

1. Open the printed `https://claude.ai/oauth/authorize…` URL in your browser and approve.
2. 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.
3. Wait for the `Paste the Claude callback URL` prompt. It appears about**15 seconds** after`Waiting 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` ):

``` php
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 pauses 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:

1. Deactivate the Custom URL connection in Amp Model Routing so threads fall back to Amp.
2. Start a new orb thread in the project and let setup finish.
3. 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.
4. **Check the Tailscale Machines page.** The old node can linger even after`disable` ; if it is still listed, remove it.
5. On the new orb, run `.agents/cliproxy-host enable` , then log in to Claude (step 6).
6. When `https://<hostname>.<tailnet>.ts.net/healthz` answers 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 that`token_use` is exactly`exchanged` . | 
| `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 in`NeedsLogin` 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` . Rerun`enable` (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 paused anyway | Host marker missing, credits exhausted, or the thread was archived. Check `.agents/cliproxy-host status` and look for`cliproxy-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_KEY` reach a provider;`/healthz` and`/` 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.
