Skip to content

2026-06-17 — claude-tmux: skip interactive-claude first-run onboarding

Problem

The claude-tmux provider drives the interactive Claude Code TUI (claude --model …) in a tmux pane. In a fresh pod it stalled: the TUI showed its first-run "Select login method" onboarding screen and waited for a keypress, ignoring CLAUDE_CODE_OAUTH_TOKEN. The pasted prompt went nowhere, the pane stayed static, and after idleStallMs (default 360s) the provider threw tmux session went idle … and fell back to claude-cli — ~6 min wasted per turn for zero observability.

Captured failure pane (via the #598 stdout pane-logging):

Welcome to Claude Code v2.1.179 … Select login method:
❯ 1. Claude account with subscription
  2. Anthropic Console account
  3. 3rd-party

Root cause

Claude Code gates its first-run onboarding flow on hasCompletedOnboarding in ~/.claude.json before consulting CLAUDE_CODE_OAUTH_TOKEN (Claude Code 2.1.x; anthropics/claude-code#28438). Headless claude -p skips onboarding — which is why claude-cli never needed this — but the interactive TUI runs the onboarding flow. The image baked /home/agent/.claude/settings.json but never created /home/agent/.claude.json (a sibling of the ~/.claude/ dir, which may be a mounted PVC), so onboarding was incomplete and the login-method picker showed. The pod runs as the non-root agent user, so this is not a root-permissions issue.

The provider's settle loop waits for a stable, non-empty pane; the static onboarding screen satisfies that, so it "settled" and pasted into a dead picker.

Fix

  1. Seed ~/.claude.json. ensureClaudeOnboarded(cwd, home) reads the existing file (if any), merges hasCompletedOnboarding: true and pre-trusts the task's workDir (projects.<cwd>.hasTrustDialogAccepted + hasCompletedProjectOnboarding), and writes it back — idempotent and key-preserving (claude rewrites the file with its own state on exit). Called before every interactive launch, using the child's HOME. The pure merge is mergeOnboardingConfig (unit-tested).
  2. Bake a baseline in Dockerfile.base: echo '{"hasCompletedOnboarding":true}' > /home/agent/.claude.json, so a fresh pod is correct even before the provider runs and for any interactive claude launched outside the provider.
  3. Pre-trust the per-task cwd. The agent uses a fresh working directory each turn; without pre-trust, the "Do you trust the files in this folder?" dialog (which isPermissionPrompt does not match) would re-prompt and stall. The seed marks each task's cwd trusted.
  4. Fail-fast safety net. isOnboardingScreen / isTrustPrompt detect a residual login-method or trust screen in both the settle loop and the poll loop; if either appears, the provider throws a clearly-labeled retryable AgentProviderError so the chain falls back to claude-cli immediately instead of idle-stalling for idleStallMs. The failure mode + pane tail reach pod stdout and the cockpit Terminal tab (via #594/#598), so a missed seed is diagnosable rather than silent.

Why this is safe / behaviour-neutral

  • No credential or billing change: the TUI still authenticates from the pod's existing CLAUDE_CODE_OAUTH_TOKEN; the fallback chain (claude-cli) is unchanged.
  • claude-cli (headless) is unaffected — it never showed onboarding.
  • The seed only sets onboarding/trust markers; claude owns the rest of ~/.claude.json.

Verification

  • Unit tests (claude-tmux.test.js): mergeOnboardingConfig (fresh / preserve / augment / no-cwd / non-object), ensureClaudeOnboarded (fresh write + merge under a temp HOME), isOnboardingScreen, isTrustPrompt. Full suite green.
  • Post-deploy verification (pending): after this merges and dev-e rolls to the new image, dev-e runs a task and the pane should reach a working session (no "Select login method"), with the previous tmux session went idle … no longer appearing in dev-e stdout.

References

  • Issue: dashecorp/rig-agent-runtime#600
  • anthropics/claude-code#28438 — onboarding gate precedes auth-token use
  • Prior diagnosability work: #594 (de-swallow fallback cause), #598 (mirror failure pane to stdout)