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
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.
[](https://www.skillsdirectory.com/skills/john-rood-anthropic-sub-proxy)
---
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.**