| 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:
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?
lsof -iTCP:<port> -sTCP:LISTEN -P
curl -s http://127.0.0.1:<port>/health
If not running, start it:
cd <proxy-dir> && nohup node index.js >> proxy.log 2>&1 &
2. Is OpenClaw actually hitting the proxy?
curl -s http://127.0.0.1:<port>/status
Also check the proxy log:
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:
find <openclaw-install-dir> -name "models.generated.js" -path "*/pi-ai/*"
grep "baseUrl" <path-to-models.generated.js> | sort -u
sed -i '' 's|https://api.anthropic.com|http://127.0.0.1:<port>|g' <path-to-models.generated.js>
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:
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:
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:
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:
curl -s http://127.0.0.1:<port>/health
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"}]}'
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 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
launchctl load ~/Library/LaunchAgents/com.claude-proxy.plist
sleep 3
launchctl list | grep claude-proxy
curl -s http://127.0.0.1:<port>/health
Test auto-restart
kill $(lsof -iTCP:<port> -sTCP:LISTEN -P -t)
sleep 6
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
launchctl unload ~/Library/LaunchAgents/com.claude-proxy.plist
launchctl unload ~/Library/LaunchAgents/com.claude-proxy.plist && \
launchctl load ~/Library/LaunchAgents/com.claude-proxy.plist
launchctl list | grep claude-proxy
After OpenClaw Updates
OpenClaw updates wipe the models.generated.js patch. After any update:
- Re-run the sed patch from step 3
- Restart OpenClaw
- 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
find ~/.openclaw -name "auth-profiles.json" -not -path "*/browser/*"
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.
import json
with open('~/.openclaw/agents/main/agent/auth-profiles.json') as f:
data = json.load(f)
data['profiles']['anthropic:default'] = {
"type": "api_key",
"provider": "anthropic",
"key": "<subscription-key>"
}
data['profiles']['anthropic:backup'] = {
"type": "api_key",
"provider": "anthropic",
"key": "<paid-api-key>"
}
Then update openclaw.json auth order:
"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
- OpenClaw reads
auth-profiles.json → finds anthropic:default → gets key field
- Passes key to pi-ai as
options.apiKey
- pi-ai sends it via
x-api-key header to the proxy (baseUrl from models.generated.js)
- 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.