Skip to content
Back to skills

Anthropic Sub Proxy

DSecurity

Troubleshoot and configure the Anthropic Subscription Proxy for OpenClaw. Use when the user has "The AI service returned an error", 401 auth failures through the proxy, key routing issues, pi-ai baseUrl overwrites, or needs help setting up the Claude Code proxy with OpenClaw. Also use when the proxy is not receiving requests, or requests bypass the proxy entirely.

  • 111 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 19, 2026
ai-agentspythongobashnodetestingapi

Works with

  • claude code
  • cli
  • api

Security analysis

D46/100
  • mediumUses curl or wget to download content
  • criticalModifies startup scripts or system services for persistence
  • criticalModifies startup scripts or system services for persistence
  • criticalExfiltrates credentials via HTTP — exact pattern from Snyk ToxicSkills study

Pro shows the line behind each finding and how to fix it

Scanned September 19, 2026

npx -y skills add John-Rood/claude-proxy --skill anthropic-sub-proxy --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Anthropic Sub Proxy?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Anthropic Sub Proxy
[![Security: D — Skills Directory](https://www.skillsdirectory.com/api/skills/john-rood-anthropic-sub-proxy/badge)](https://www.skillsdirectory.com/skills/john-rood-anthropic-sub-proxy)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: anthropic-sub-proxy
description: Troubleshoot and configure the Anthropic Subscription Proxy for OpenClaw. Use when the user has "The AI service returned an error", 401 auth failures through the proxy, key routing issues, pi-ai baseUrl overwrites, or needs help setting up the Claude Code proxy with OpenClaw. Also use when the proxy is not receiving requests, or requests bypass the proxy entirely.
---

# Anthropic Subscription Proxy — Troubleshooting

The proxy is a lightweight Node.js server (~150 lines, zero dependencies) that sits between OpenClaw and api.anthropic.com. It adds the Claude Code envelope (beta header + system prompt) to requests so Claude Code subscription billing applies instead of direct API billing.

**Proxy repo:** Separate from this skill. The user should already have `index.js` and `config.json` deployed.

## How It Works

```
OpenClaw → localhost:<port> → Proxy → api.anthropic.com
                                ↓
                          1. Pass through client's API key unchanged
                          2. Add anthropic-beta: claude-code-20250219
                          3. Prepend "You are Claude Code..." system prompt
                          4. Stream response back untouched
```

The proxy is **passthrough by default** — it forwards whatever API key the client sends. No keys need to be configured in the proxy itself.

## Critical: How Keys Actually Work

### The only key that reliably works

The key managed by Claude Code CLI in the **macOS Keychain** (`sk-ant-api03-...`). This is the key tied to whichever account is currently logged into Claude Code CLI. It gets auto-synced into OpenClaw's auth system at runtime, regardless of what's in config files.

### setup-token keys (`sk-ant-oat01-...`) are unreliable

Tokens generated via `claude setup-token` consistently arrive invalid (401 `invalid x-api-key`) even when generated moments before testing. The reason is unknown — they may not be activating server-side, or they may require a different auth flow. **Do not rely on these for the proxy.**

### What this means

- **One subscription at a time.** Claude Code CLI only stores one account's credentials. The proxy can only use that one key.
- **auth-profiles.json is misleading.** OpenClaw's auth system auto-syncs from the Keychain on every load, overriding whatever is manually configured. The token Claudius actually sends may not match any named profile in the config.
- **Round-robin across multiple subscriptions is not currently possible** unless multiple valid `sk-ant-api03` keys can be captured from separate Claude Code CLI sessions (which requires logging in and out — and logging out may revoke the previous key).

### To identify which key is actually being used

Check the runtime auth log, not the config files:
```bash
grep "ANTHROPIC-AUTH" <openclaw-log> | tail -5
```

The `tokenPrefix` field shows what's actually being sent. Compare against:
- Config keys in `auth-profiles.json`
- Keychain-synced keys (auto-injected, no profile name)

If the token prefix doesn't match any configured profile, it's coming from the Keychain sync.

## Diagnostic Checklist

When the user reports errors, run through these checks in order:

### 1. Is the proxy running?

```bash
lsof -iTCP:<port> -sTCP:LISTEN -P
curl -s http://127.0.0.1:<port>/health
```

If not running, start it:
```bash
cd <proxy-dir> && nohup node index.js >> proxy.log 2>&1 &
```

### 2. Is OpenClaw actually hitting the proxy?

```bash
curl -s http://127.0.0.1:<port>/status
# Check requestsServed — if 0 after sending a message, requests aren't reaching the proxy
```

Also check the proxy log:
```bash
tail -20 <proxy-dir>/proxy.log
```

### 3. Is the baseUrl override being respected?

**This is the #1 issue.** OpenClaw's pi-ai model catalog (`models.generated.js`) hardcodes `baseUrl` for every model, **overriding** whatever is in the OpenClaw config.

Find and patch the model catalog:
```bash
# Find the file
find <openclaw-install-dir> -name "models.generated.js" -path "*/pi-ai/*"

# Check current Anthropic baseUrls
grep "baseUrl" <path-to-models.generated.js> | sort -u

# Patch all Anthropic models to point at the proxy
sed -i '' 's|https://api.anthropic.com|http://127.0.0.1:<port>|g' <path-to-models.generated.js>

# Verify
grep -c "127.0.0.1:<port>" <path-to-models.generated.js>
```

**⚠️ This patch gets wiped on OpenClaw updates/reinstalls.** Re-run after any update.

### 4. Auth token issues (401 errors)

If the proxy returns 401 or Anthropic returns 401:

**Check what token OpenClaw is actually sending** — look for `ANTHROPIC-AUTH` in OpenClaw's logs:
```bash
grep "ANTHROPIC-AUTH" <openclaw-log> | tail -5
```

Common problems:

#### a. OpenClaw syncs tokens from Claude Code's Keychain
OpenClaw's auth system auto-syncs credentials from Claude Code CLI's macOS Keychain on every load. This **overrides** whatever is in `auth-profiles.json`. If old/expired tokens exist in the Keychain, OpenClaw will use them.

**Fix:** The proxy should NOT validate client keys. Ensure the proxy accepts any key (passthrough mode). The proxy's job is only to add the envelope, not to authenticate.

#### b. Multiple auth profiles cycling
If OpenClaw has multiple Anthropic auth profiles (manual, manual2, manual3, default), it round-robins through them. Some may be dead/expired.

**Check config:**
```bash
# Show auth order
cat <openclaw-config> | python3 -c "
import json, sys
cfg = json.load(sys.stdin)
order = cfg.get('auth',{}).get('order',{}).get('anthropic',[])
print('Auth order:', order)
profiles = cfg.get('auth',{}).get('profiles',{})
for name, p in profiles.items():
    if 'anthropic' in name:
        print(f'  {name}: mode={p.get(\"mode\",\"?\")}')"
```

**Fix:** Either ensure all profiles have valid keys, or trim the auth order to only valid profiles.

#### c. OAuth tokens expire
OAuth tokens (`sk-ant-oat01-...`) expire and need refresh. If refresh fails, the request fails.

**Fix:** Remove dead OAuth profiles from config and auth-profiles.json. Keep only profiles with valid, non-expired credentials.

### 5. Proxy returns 502 (upstream error)

The proxy connected to Anthropic but got an error. Check:
- Is the API key valid? Test directly: `curl -s https://api.anthropic.com/v1/messages -H "x-api-key: <key>" -H "anthropic-version: 2023-06-01" -H "content-type: application/json" -d '{"model":"claude-sonnet-4-20250514","max_tokens":10,"messages":[{"role":"user","content":"hi"}]}'`
- Does the key's org have overage/extra usage enabled?
- Is the account suspended or rate-limited?

### 6. Proxy works but response is wrong/truncated

Check if streaming is passing through correctly:
```bash
# Test streaming
curl -s http://127.0.0.1:<port>/v1/messages \
  -H "Content-Type: application/json" \
  -H "x-api-key: <valid-key>" \
  -H "anthropic-version: 2023-06-01" \
  -d '{"model":"claude-sonnet-4-20250514","max_tokens":50,"stream":true,"messages":[{"role":"user","content":"Say hi"}]}'
```

## Quick Validation Test

Run this end-to-end test to confirm the full chain works:

```bash
# 1. Health check
curl -s http://127.0.0.1:<port>/health

# 2. Non-streaming test
curl -s http://127.0.0.1:<port>/v1/messages \
  -H "Content-Type: application/json" \
  -H "x-api-key: <valid-anthropic-key>" \
  -H "anthropic-version: 2023-06-01" \
  -d '{"model":"claude-sonnet-4-20250514","max_tokens":50,"messages":[{"role":"user","content":"Say hi in 5 words"}]}'

# 3. Check it was logged
tail -5 <proxy-dir>/proxy.log
```

## Persistent Service (macOS launchd)

The proxy should run as a persistent macOS service so it auto-restarts on crash and starts on boot. Without this, the proxy will eventually die and OpenClaw will get connection errors.

### Create the plist

Write to `~/Library/LaunchAgents/com.claude-proxy.plist`:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.claude-proxy</string>
    <key>ProgramArguments</key>
    <array>
        <string>/path/to/node</string>
        <string>/path/to/claude-proxy/index.js</string>
    </array>
    <key>WorkingDirectory</key>
    <string>/path/to/claude-proxy</string>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <dict>
        <key>SuccessfulExit</key>
        <false/>
    </dict>
    <key>StandardOutPath</key>
    <string>/tmp/claude-proxy-stdout.log</string>
    <key>StandardErrorPath</key>
    <string>/tmp/claude-proxy-stderr.log</string>
    <key>ThrottleInterval</key>
    <integer>10</integer>
    <key>EnvironmentVariables</key>
    <dict>
        <key>PATH</key>
        <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
    </dict>
</dict>
</plist>
```

Replace `/path/to/node` with the actual node binary (e.g. `which node` or `/opt/homebrew/Cellar/node/<version>/bin/node`).

### Load and verify

```bash
launchctl load ~/Library/LaunchAgents/com.claude-proxy.plist
sleep 3
launchctl list | grep claude-proxy   # Should show PID and exit code 0
curl -s http://127.0.0.1:<port>/health
```

### Test auto-restart

```bash
# Kill the proxy
kill $(lsof -iTCP:<port> -sTCP:LISTEN -P -t)
sleep 6
# Verify it came back with a new PID
lsof -iTCP:<port> -sTCP:LISTEN -P
```

### Common launchd issues

- **Exit code 78 ("Function not implemented")**: Node binary path is wrong or launchd can't find it. Use the full absolute path to the node binary, not a symlink. Add PATH in EnvironmentVariables.
- **KeepAlive with `true`**: Can cause aggressive restart loops. Use `SuccessfulExit: false` instead — this only restarts on crashes, not clean exits.
- **Logs**: Check `/tmp/claude-proxy-stdout.log` and `/tmp/claude-proxy-stderr.log` for startup errors.

### Manage the service

```bash
# Stop
launchctl unload ~/Library/LaunchAgents/com.claude-proxy.plist

# Restart
launchctl unload ~/Library/LaunchAgents/com.claude-proxy.plist && \
launchctl load ~/Library/LaunchAgents/com.claude-proxy.plist

# Check status
launchctl list | grep claude-proxy
```

## After OpenClaw Updates

OpenClaw updates wipe the `models.generated.js` patch. After any update:
1. Re-run the sed patch from step 3
2. Restart OpenClaw
3. Verify with a test message

## ⚠️ Lesson Learned: Key Configuration (2026-04-11)

**The canonical key store is `auth-profiles.json`, NOT env vars.**

When setting up or changing Anthropic keys for the proxy:

### DO THIS FIRST
```bash
# Find the auth-profiles file
find ~/.openclaw -name "auth-profiles.json" -not -path "*/browser/*"
# → ~/.openclaw/agents/main/agent/auth-profiles.json

# Read it — this is where the actual key lives
cat ~/.openclaw/agents/main/agent/auth-profiles.json
```

### Edit the key directly in auth-profiles.json
The `anthropic:default` profile's `key` field is what OpenClaw actually sends. Change it there.

```python
# Example: swap to subscription key as primary, paid key as backup
import json
with open('~/.openclaw/agents/main/agent/auth-profiles.json') as f:
    data = json.load(f)

# Set subscription key as default
data['profiles']['anthropic:default'] = {
    "type": "api_key",
    "provider": "anthropic", 
    "key": "<subscription-key>"
}

# Add paid key as backup
data['profiles']['anthropic:backup'] = {
    "type": "api_key",
    "provider": "anthropic",
    "key": "<paid-api-key>"
}
```

Then update `openclaw.json` auth order:
```json
"anthropic": ["anthropic:default", "anthropic:backup"]
```

One gateway restart. Done.

### What DOESN'T work (or is unnecessary)
- ❌ `~/.openclaw/.env` with `ANTHROPIC_API_KEY` — auth-profiles.json takes priority
- ❌ `OPENCLAW_LIVE_ANTHROPIC_KEYS` env var — same, auth-profiles wins
- ❌ Adding vars to launchd plist — auth-profiles.json is checked first
- ❌ Searching macOS Keychain — OpenClaw doesn't store Anthropic keys there
- ❌ Restarting the proxy with debug logging mid-tool-call — kills your own connection

### How key resolution actually works
1. OpenClaw reads `auth-profiles.json` → finds `anthropic:default` → gets `key` field
2. Passes key to pi-ai as `options.apiKey`
3. pi-ai sends it via `x-api-key` header to the proxy (baseUrl from models.generated.js)
4. Proxy adds Claude Code envelope → forwards to Anthropic with the same key

**Trace the path BEFORE touching anything. Don't gunshoot env vars hoping one sticks.**

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…