Authenticate Claude CLI on headless servers via tmux and local Playwright OAuth flow, generating subscription tokens for remote usage.
How this skill is triggered — by the user, by Claude, or both
Slash command
/integrations:claude-server-authThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Authenticate Claude CLI on headless servers (no GUI browser) using `setup-token` in tmux + local Playwright for OAuth.
Authenticate Claude CLI on headless servers (no GUI browser) using setup-token in tmux + local Playwright for OAuth.
CLAUDE_CODE_OAUTH_TOKEN for subscription-based usagessh your-server)npm i -g @anthropic-ai/claude-code)Local PC Remote Server (headless)
----------- -------------------------
Playwright browser <-- URL --- tmux: claude setup-token
| |
v |
Google OAuth login |
| |
v |
Claude "Authorize" |
| |
v v
callback URL ---- code --------> tmux paste-buffer
(code#state) |
v
stdout: sk-ant-oat01-...
ACCOUNT="account-1"
SESSION="auth1"
ssh your-server "mkdir -p /root/.claude-accounts/$ACCOUNT"
ssh your-server "tmux kill-session -t $SESSION 2>/dev/null; \
rm -f /tmp/${SESSION}-output.txt; \
tmux new-session -d -s $SESSION -x 400 -y 30 \
'CLAUDE_CONFIG_DIR=/root/.claude-accounts/$ACCOUNT claude setup-token'"
Key flags:
-x 400 wide terminal prevents URL line-wrapping-d detached so SSH doesn't need to hold TTYCLAUDE_CONFIG_DIR isolates auth per accountsleep 4
ssh your-server "tmux pipe-pane -t $SESSION 'cat >> /tmp/${SESSION}-output.txt'"
ssh your-server "tmux capture-pane -t $SESSION -p -S -50"
IMPORTANT: Set up pipe-pane BEFORE pasting the code. This captures output even if the tmux session exits (setup-token exits immediately after printing the token).
Look for URL like:
https://claude.ai/oauth/authorize?code=true&client_id=...&state=...
If URL is truncated, use -S -100 for more scrollback.
browser_navigate to the captured URL
Wait 3-4 seconds for the page to load, then take a snapshot.
Three possible scenarios after opening the URL:
A) "Authorize" button visible with correct account — Skip to step 5.
B) "Authorize" button visible with WRONG account — Click "Switch account" link at the bottom to log out and re-enter with the correct account.
C) Login page — Need Google OAuth:
KNOWN BUG: The first click on "Authorize" often fails silently (returns a 403 on the server-side authorize endpoint). The page stays the same.
Fix: Wait 3-5 seconds, check if the page still shows "Authorize". If yes, click again. The second click typically works.
# Click Authorize
browser_click "Authorize" ref=...
# Wait and check
browser_wait_for time=3
browser_snapshot
# If still on authorize page — click again
browser_click "Authorize" ref=...
After successful Authorize click, the page redirects to:
https://platform.claude.com/oauth/code/callback?code=XXXX&state=YYYY
The page shows "Authentication Code" with the full code displayed as:
XXXX#YYYY
The code is shown directly on the page — no need to parse the URL. Copy the full value from the page element (ref e19 in snapshot typically).
IMPORTANT: The # between code and state is literal, not a URL fragment.
DO NOT use tmux send-keys — the # character breaks it even with -l flag.
Use load-buffer + paste-buffer:
ssh your-server "tmux load-buffer - <<'EOF'
THE_CODE#THE_STATE
EOF"
ssh your-server "tmux paste-buffer -t $SESSION && sleep 1 && tmux send-keys -t $SESSION Enter"
NOTE: Use <<'EOF' (quoted) to prevent shell interpretation of special characters in the code.
Wait 5-6 seconds for token generation:
sleep 5
ssh your-server "tmux capture-pane -t $SESSION -p -S -100 2>/dev/null || cat /tmp/${SESSION}-output.txt"
Token appears in the output as: sk-ant-oat01- followed by ~80+ characters.
The output contains ANSI escape codes — extract the token by looking for sk-ant-oat01- pattern.
If tmux session already exited, the pipe-pane fallback (/tmp/${SESSION}-output.txt) will have the output.
KNOWN ISSUE: setup-token frequently returns "OAuth error: Request failed with status code 500" — this is an intermittent server-side error from Anthropic.
Fix: The CLI shows "Press Enter to retry". Two approaches:
A) Retry in-place (if the session is still alive):
ssh your-server "tmux send-keys -t $SESSION Enter"
# Wait for new URL
sleep 5
ssh your-server "tmux capture-pane -t $SESSION -p -S -50"
# Get new URL, repeat from step 3
B) Full restart (cleaner, recommended after 2+ failures):
ssh your-server "tmux kill-session -t $SESSION 2>/dev/null; \
rm -f /tmp/${SESSION}-output.txt; \
tmux new-session -d -s $SESSION -x 400 -y 30 \
'CLAUDE_CONFIG_DIR=/root/.claude-accounts/$ACCOUNT claude setup-token'"
# Re-setup pipe-pane and repeat from step 2
The 500 error is random and may happen 1-3 times before succeeding. It may take 1-3 retries before it succeeds. Don't give up.
NOTE: Each retry generates a new code_challenge and state. The browser must re-authorize with the new URL — old codes won't work with a new challenge.
ssh your-server "echo 'sk-ant-oat01-...' > /root/.claude-accounts/$ACCOUNT/token.txt"
ssh your-server "chmod 600 /root/.claude-accounts/$ACCOUNT/token.txt"
# Verify — use single quotes for SSH to avoid local shell expansion
ssh your-server 'CLAUDE_CODE_OAUTH_TOKEN=$(cat /root/.claude-accounts/$ACCOUNT/token.txt) claude -p --model claude-haiku-4-5-20251001 --output-format text "respond with just OK"'
# Expected: OK
IMPORTANT: Use single quotes around the SSH command to prevent local $() expansion. With double quotes, the subshell runs locally and fails.
ssh your-server "tmux kill-session -t $SESSION 2>/dev/null; rm -f /tmp/${SESSION}-output.txt"
When authenticating multiple accounts sequentially, the browser stays logged into the previous Claude account.
To switch accounts:
TIP: After switching, always verify "Logged in as [email protected]" at the bottom of the Authorize page before clicking.
Rotation script at /root/.claude-accounts/token-rotator.sh:
token-rotator.sh status # show all accounts and which is active
token-rotator.sh get # get current token (no rotation)
token-rotator.sh rotate # switch to next account
token-rotator.sh validate # test current token with haiku
token-rotator.sh get-valid # get token with auto-failover (tries up to 3)
token-rotator.sh init # re-scan account directories
Usage in projects:
export CLAUDE_CODE_OAUTH_TOKEN=$(bash /root/.claude-accounts/token-rotator.sh get-valid)
claude -p "your prompt"
Auto-failover logic: get-valid tries current token → if error → rotates → tries next → up to 3 attempts.
Update auth-profiles.json with all subscription tokens:
{
"version": 1,
"profiles": {
"anthropic:subscription-1": {
"type": "token",
"provider": "anthropic",
"token": "sk-ant-oat01-..."
},
"anthropic:subscription-2": {
"type": "token",
"provider": "anthropic",
"token": "sk-ant-oat01-..."
},
"anthropic:subscription-3": {
"type": "token",
"provider": "anthropic",
"token": "sk-ant-oat01-..."
}
},
"order": {
"anthropic": [
"anthropic:subscription-1",
"anthropic:subscription-2",
"anthropic:subscription-3"
]
},
"lastGood": {
"anthropic": "anthropic:subscription-1"
}
}
Path: /var/lib/docker/volumes/your-config-volume/_data/agents/main/agent/auth-profiles.json
IMPORTANT: Set ownership chown 1000:1000 (node user inside container).
After updating, restart gateway:
cd ~/your-gateway && docker compose -f docker-compose.hardened.yml restart your-gateway
auth gateway failover: tries tokens in order sequence. If first token hits rate limit, automatically tries the next.
| Mistake | Why it fails | Fix |
|---|---|---|
tmux send-keys -l "code#state" | # interpreted as window ref | Use load-buffer + paste-buffer |
claude setup-token | tee log | Pipe breaks Ink TUI raw mode | Use tmux pipe-pane for capture |
claude auth login for paste flow | auth login uses polling, no paste prompt | Use setup-token instead |
| Running Playwright on server | Cloudflare blocks headless browsers on claude.ai | Run Playwright locally |
Expecting .credentials.json | setup-token outputs to stdout only | Capture from tmux output |
| Narrow tmux window | URL gets line-wrapped and truncated | Use -x 400 |
ssh your-server "claude setup-token" | No TTY for Ink TUI | Must use tmux |
ssh your-server "OAUTH=\$(cat ...)" | $() expands locally, not on server | Use single-quoted SSH: ssh your-server '...' |
| Clicking Authorize only once | First click often fails silently (403) | Wait 3s, check page, click again if needed |
| Reusing code after 500 error | Each retry generates new code_challenge | Must re-authorize in browser with new URL |
| Not setting up pipe-pane early | Token output lost if tmux exits | Set pipe-pane right after session creation |
Forgetting chown 1000:1000 | auth gateway container can't read profiles | Always chown after writing auth-profiles.json |
auth login instead of setup-token (different flow, no paste prompt)CLAUDE_CONFIG_DIR when managing multiple accounts (overwrites single config)setup-token in tmux with -x 400 wide terminaltmux pipe-pane for output capturecode#state from callback pageload-buffer + paste-buffer (NOT send-keys)sk-ant-oat01-... token from outputchmod 600'...' single-quoted SSH commandtoken-rotator.sh init)chown 1000:1000npx claudepluginhub jhamidun/claude-code-config-pack --plugin integrationsSets up isolated workspaces using native worktree tools or git worktree fallback. Use before starting feature work to protect the current branch.