From cf6400e8f3564507f4abab12974f5d388a6d76d6 Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 25 Mar 2026 17:48:21 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20shared=20Claude=20OAuth=20credentials=20?= =?UTF-8?q?in=20containers=20=E2=80=94=20mount=20+=20flock=20to=20prevent?= =?UTF-8?q?=20token=20rotation=20race=20(#693)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Make ~/.claude volume mount read-write (was :ro) so containers can write back refreshed OAuth tokens - Wrap Claude CLI in flock(1) inside tmux sessions using ~/.claude/session.lock — prevents concurrent token refresh races across agents sharing the same credentials - Add ANTHROPIC_API_KEY detection in entrypoint.sh: when set, skips OAuth entirely (no rotation issues, metered billing) - Log active auth method (API key vs OAuth vs missing) at container startup for easier 401 debugging - Document 'claude auth login' requirement in disinto init output Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/disinto | 10 +++++++++- docker/agents/entrypoint.sh | 12 ++++++++++++ lib/agent-session.sh | 14 +++++++++++++- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/bin/disinto b/bin/disinto index 0578cbe..1c7cab2 100755 --- a/bin/disinto +++ b/bin/disinto @@ -223,7 +223,7 @@ services: - agent-data:/home/agent/data - project-repos:/home/agent/repos - ./:/home/agent/disinto:ro - - ${HOME}/.claude:/home/agent/.claude:ro + - ${HOME}/.claude:/home/agent/.claude - ${HOME}/.claude.json:/home/agent/.claude.json:ro - CLAUDE_BIN_PLACEHOLDER:/usr/local/bin/claude:ro environment: @@ -1517,6 +1517,14 @@ p.write_text(text) else echo " Mode: bare-metal" fi + echo "" + echo "── Claude authentication ──────────────────────────────" + echo " OAuth (shared across containers):" + echo " Run 'claude auth login' on the host once." + echo " Credentials in ~/.claude are mounted into containers." + echo " API key (alternative — metered billing, no rotation issues):" + echo " Set ANTHROPIC_API_KEY in .env to skip OAuth entirely." + echo "" echo " Run 'disinto status' to verify." } diff --git a/docker/agents/entrypoint.sh b/docker/agents/entrypoint.sh index 9b83d32..4fc3d17 100644 --- a/docker/agents/entrypoint.sh +++ b/docker/agents/entrypoint.sh @@ -55,6 +55,18 @@ if ! command -v claude &>/dev/null; then fi log "Claude CLI: $(claude --version 2>&1 || true)" +# ANTHROPIC_API_KEY fallback: when set, Claude uses the API key directly +# and OAuth token refresh is not needed (no rotation race). Log which +# auth method is active so operators can debug 401s. +if [ -n "${ANTHROPIC_API_KEY:-}" ]; then + log "Auth: ANTHROPIC_API_KEY is set — using API key (no OAuth rotation)" +elif [ -f /home/agent/.claude/credentials.json ]; then + log "Auth: OAuth credentials mounted from host (~/.claude)" +else + log "WARNING: No ANTHROPIC_API_KEY and no OAuth credentials found." + log "Run 'claude auth login' on the host, or set ANTHROPIC_API_KEY in .env" +fi + install_project_crons # Configure tea CLI login for forge operations (runs as agent user). diff --git a/lib/agent-session.sh b/lib/agent-session.sh index d5cae19..bc0b93f 100644 --- a/lib/agent-session.sh +++ b/lib/agent-session.sh @@ -287,8 +287,20 @@ create_agent_session() { if [ -n "${CLAUDE_MODEL:-}" ]; then model_flag="--model ${CLAUDE_MODEL}" fi + + # Acquire a session-level mutex via flock to prevent concurrent Claude + # sessions from racing on OAuth token refresh (rotating the refresh token + # invalidates it for other sessions). The lock is held for the lifetime + # of the Claude process inside the tmux session. + # Use ~/.claude/session.lock so the lock is shared across containers when + # the host ~/.claude directory is bind-mounted. + local lock_dir="${HOME}/.claude" + mkdir -p "$lock_dir" + local claude_lock="${lock_dir}/session.lock" + local claude_cmd="flock -n ${claude_lock} claude --dangerously-skip-permissions ${model_flag}" + tmux new-session -d -s "$session" -c "$workdir" \ - "claude --dangerously-skip-permissions ${model_flag}" 2>/dev/null + "$claude_cmd" 2>/dev/null sleep 1 tmux has-session -t "$session" 2>/dev/null || return 1 agent_wait_for_claude_ready "$session" 120 || return 1