You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/agents/index.md
+147Lines changed: 147 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -261,6 +261,153 @@ vp run claude # or: vp c
261
261
262
262
Credentials are stored in `~/.config/vibepod/agents/claude/`. On first run, Claude's interactive setup will guide you through API key configuration.
263
263
264
+
#### Long-lived token (recommended)
265
+
266
+
Claude Code has a known upstream bug where OAuth access tokens (~8 h TTL) are not automatically refreshed from disk, forcing users to run `/login` roughly once per day. See [Why this workaround exists](#why-this-workaround-exists) below for the full bug history and links.
267
+
268
+
VibePod works around this by storing a ~1-year long-lived token on the host and injecting it as `CLAUDE_CODE_OAUTH_TOKEN` on every run. This sidesteps the refresh path entirely.
269
+
270
+
!!! info "This is an official authentication method"
271
+
`claude setup-token`and the `CLAUDE_CODE_OAUTH_TOKEN` environment variable are both documented by Anthropic as a supported authentication path for CI pipelines, scripts, and other environments where an interactive browser login isn't available. See the [official Claude Code authentication docs](https://code.claude.com/docs/en/authentication#long-lived-tokens) and the [`claude-code-action` setup guide](https://github.com/anthropics/claude-code-action/blob/main/docs/setup.md). VibePod just automates the storage and injection.
272
+
273
+
**One-time setup:**
274
+
275
+
```bash
276
+
vp run claude setup-token
277
+
```
278
+
279
+
This starts the container with `claude setup-token`, which opens Anthropic's OAuth flow in your browser. After you authorise, the container prints a token. VibePod then prompts you to paste it and saves it to:
VibePod detects the stored token and injects `CLAUDE_CODE_OAUTH_TOKEN` automatically. Look for `Using stored Claude OAuth token` in the startup output to confirm.
292
+
293
+
**Precedence** (first match wins):
294
+
295
+
1. `-e ANTHROPIC_API_KEY=...` or `-e CLAUDE_CODE_OAUTH_TOKEN=...` passed on the CLI
296
+
2. `ANTHROPIC_API_KEY` or `CLAUDE_CODE_OAUTH_TOKEN` set in your per-agent `env:` config
297
+
3. Stored `oauth-token` file
298
+
4. Interactive OAuth via `.credentials.json` (subject to the refresh bug)
299
+
300
+
**Verifying the token is stored:**
301
+
302
+
```bash
303
+
vp doctor claude
304
+
```
305
+
306
+
Shows credentials state, stored-token presence and mtime, and which auth mode the next run will use. You can also inspect the file directly:
307
+
308
+
```bash
309
+
ls -l ~/.config/vibepod/agents/claude/oauth-token
310
+
# or to view contents (treat as a secret — do not share):
311
+
nano ~/.config/vibepod/agents/claude/oauth-token
312
+
```
313
+
314
+
**Verifying the token works:**
315
+
316
+
```bash
317
+
vp run claude -p "say ok"
318
+
```
319
+
320
+
`-p`runs Claude Code in headless mode — one API call, one response. If you see "ok", the token is valid.
321
+
322
+
**Caveats:**
323
+
324
+
- The long-lived token is **inference-only** — it cannot establish [Remote Control](https://code.claude.com/docs/en/remote-control) sessions (steering a container from claude.ai/code or the mobile app).
325
+
- `claude setup-token`requires a **Pro, Max, Team, or Enterprise** plan. Console (pay-per-token) accounts should use `ANTHROPIC_API_KEY` instead.
326
+
- The token rotates roughly once a year. When it expires, just run `vp run claude setup-token` again.
327
+
328
+
#### Using an API key instead
329
+
330
+
If you're on a Console (pay-per-token) account, set `ANTHROPIC_API_KEY` and skip the setup-token flow entirely:
331
+
332
+
```bash
333
+
vp run claude -e ANTHROPIC_API_KEY=sk-ant-...
334
+
```
335
+
336
+
Or permanently in config:
337
+
338
+
```yaml
339
+
agents:
340
+
claude:
341
+
env:
342
+
ANTHROPIC_API_KEY: sk-ant-...
343
+
```
344
+
345
+
#### Diagnostics
346
+
347
+
`vp doctor claude` is the first tool to reach for when auth misbehaves. It reports:
348
+
349
+
- `.credentials.json`— file owner/mode, `expiresAt`, presence of `refreshToken`, scopes, subscription type
350
+
- `.claude.json`— mtime cross-check
351
+
- Stored long-lived token state
352
+
- Which host env vars (`ANTHROPIC_API_KEY`, `CLAUDE_CODE_OAUTH_TOKEN`, `CLAUDE_CONFIG_DIR`) are set
353
+
- **Effective auth mode** — what the next `vp run claude` will actually use
354
+
355
+
Exit codes: `0`healthy, `1` config dir missing, `2` OAuth token expired (useful in scripts).
356
+
357
+
#### Why this workaround exists
358
+
359
+
The root cause is in Claude Code itself, not in VibePod. The OAuth `refreshToken` is stored in `.credentials.json` but never used: the access token is loaded from disk, sent as-is until it 401s, and nothing is written back when a refresh would have succeeded. The bug affects native Linux, WSL, macOS, and every container-based deployment equally.
360
+
361
+
Community forensics ([#33995 comment](https://github.com/anthropics/claude-code/issues/33995#issuecomment-2718892341)):
362
+
363
+
> Set `expiresAt` in `~/.claude/.credentials.json` to `Date.now()` to force expiry. Send a message — Claude processes it successfully, meaning the in-memory token refresh worked. Check `~/.claude/.credentials.json` afterward — file was never written. Conclusion: `refreshOAuthToken` succeeds and returns new tokens, but the credential store's `update()` is never called (or silently fails) after a successful refresh. The new token lives only in memory. Next session launch reads the stale expired token from disk and requires re-login.
364
+
365
+
The community-validated workaround ([#24317 comment](https://github.com/anthropics/claude-code/issues/24317#issuecomment-2664923815)) is exactly what VibePod implements:
366
+
367
+
> I worked around this using `claude setup-token` and then feeding it in as the `CLAUDE_CODE_OAUTH_TOKEN` environment variable. It skips all the "OAuth tokens invalidating each other", but has the downside that it doesn't allow `/usage`.
368
+
369
+
`claude setup-token`itself is an **officially supported** Claude Code authentication path, documented for exactly this kind of non-interactive deployment. See Anthropic's [authentication guide](https://code.claude.com/docs/en/authentication#long-lived-tokens) and the [`claude-code-action` setup guide](https://github.com/anthropics/claude-code-action/blob/main/docs/setup.md) — the same mechanism used by Anthropic's own GitHub Action.
370
+
371
+
**Upstream tracking issues — core bug (access-token not refreshed from disk):**
372
+
373
+
| # | Status | Summary |
374
+
|---|---|---|
375
+
| [#50743](https://github.com/anthropics/claude-code/issues/50743) | open · `has repro` · `area:auth` | Newest and cleanest repro on headless Linux — `refreshToken` ignored |
376
+
| [#42904](https://github.com/anthropics/claude-code/issues/42904) | closed as duplicate | Canonical "daily re-login required for subscription users" report |
377
+
| [#40985](https://github.com/anthropics/claude-code/issues/40985) | open · `stale` | "Auth tokens expire too frequently" — confirms ~8 h TTL |
378
+
| [#33995](https://github.com/anthropics/claude-code/issues/33995) | closed not-planned | Best technical forensics (quoted above); proves write-back is the broken step |
379
+
| [#21765](https://github.com/anthropics/claude-code/issues/21765) | closed not-planned | First clear statement: "Claude Code doesn't use refresh tokens to get new access tokens" |
| [#37402](https://github.com/anthropics/claude-code/issues/37402) | open | `--print` / automation mode also affected |
382
+
383
+
**Multi-session race condition** (why a shared `.credentials.json` across simultaneous sessions makes things worse):
384
+
385
+
| # | Status | Summary |
386
+
|---|---|---|
387
+
| [#24317](https://github.com/anthropics/claude-code/issues/24317) | open · `has repro` · 18 comments | Canonical thread; documents refresh-token rotation and single-use semantics |
388
+
| [#48786](https://github.com/anthropics/claude-code/issues/48786) | closed as dup of #24317 | Independent reproduction |
389
+
| [#27933](https://github.com/anthropics/claude-code/issues/27933) | closed | Early race-condition report |
390
+
| [#45129](https://github.com/anthropics/claude-code/issues/45129) | closed as dup | Agent worktree subprocesses hit this constantly |
391
+
392
+
**Container / headless specifically:**
393
+
394
+
| # | Status | Summary |
395
+
|---|---|---|
396
+
| [#22066](https://github.com/anthropics/claude-code/issues/22066) | closed as duplicate | OAuth authentication not persisting in Docker |
397
+
| [#34917](https://github.com/anthropics/claude-code/issues/34917) | closed | OAuth "Redirect URI not supported" in headless/Docker |
398
+
| [#34141](https://github.com/anthropics/claude-code/issues/34141) | closed | Claude Code ignores `ANTHROPIC_API_KEY` when OAuth redirect fails in devcontainers |
399
+
| [#7100](https://github.com/anthropics/claude-code/issues/7100) | closed not-planned | Request for official headless-auth documentation |
400
+
| [#22992](https://github.com/anthropics/claude-code/issues/22992) | open | Feature request: RFC 8628 device-code flow for headless |
401
+
402
+
**Proxy / Cloudflare interaction** (relevant if you run vibepod behind the built-in mitmproxy):
403
+
404
+
| # | Status | Summary |
405
+
|---|---|---|
406
+
| [#47754](https://github.com/anthropics/claude-code/issues/47754) | open · `area:auth` · `platform:linux` | Cloudflare WAF blocks OAuth token refresh from headless Linux servers |
407
+
| [#33269](https://github.com/anthropics/claude-code/issues/33269) | open | Cloudflare challenge race during `auth login` / `setup-token` |
408
+
409
+
**Anthropic's posture:** most reports are auto-closed as duplicates by a bot; the core issues (#21765, #33995) were closed as "not planned." A changelog line for Claude Code v2.1.44 mentioned "Fixed auth refresh errors" but users report the same behaviour on every later version (v2.1.62, 2.1.74, 2.1.116 observed). No committed fix has landed as of this writing.
0 commit comments