#!/bin/bash # anthropic-cloud-paul-oauth init — write Claude Code's native credentials # file from the ESO-mounted oauth_token. # # Threat model: keep the ESO mount root-only (mode 0400) so the agent user # cannot directly `cat` /run/agent/secrets/anthropic-cloud-paul-oauth/*. # init.sh runs as root (in uid-wrapper.sh, before the gosu drop) and writes # a per-secret artefact at $AGENT_HOME/.claude/.credentials.json with mode # 0600 owned by agent. Claude Code reads that file natively for OAuth-based # subscription auth — no env vars, no apiKeyHelper, no wrapper script. # # Why .credentials.json (not apiKeyHelper): # - This is the SUBSCRIPTION (Pro/Max) path: ENFORCE_SUBSCRIPTION_PRICING=true # on this harness causes the runner to strip ANTHROPIC_API_KEY and # ANTHROPIC_AUTH_TOKEN from the subprocess env so an accidental API key # can't fall through to per-token billing. Subscription auth flows # through CLAUDE_CODE_OAUTH_TOKEN — Claude Code's native storage for # that is .credentials.json with the claudeAiOauth shape. # - apiKeyHelper would still work, but routes through the api_key path, # which the runner's subscription-pricing enforcement is specifically # designed to block. .credentials.json is the canonical OAuth path. # # This mirrors what entrypoint/harness_init.py used to do for the legacy # SOPS-decrypted CLAUDE_CODE_OAUTH_TOKEN path. Phase 9 ESO-cutover removed # the SOPS files; this init.sh restores the equivalent behaviour for the # ESO-mounted token. # # Rotation handling: per-container init. Ephemeral container agents (one # task = one container) always pick up the latest mounted oauth_token. # Long-running sessions need a future scripts.control_loop hook to # re-stage between agent CLI invocations. set -euo pipefail ESO_OAUTH="/run/agent/secrets/anthropic-cloud-paul-oauth/oauth_token" if [ ! -r "$ESO_OAUTH" ]; then echo "ERROR: $ESO_OAUTH not readable. Check ESO ExternalSecret acct-." >&2 exit 1 fi AGENT_USER="${AGENT_USER:-agent}" AGENT_HOME=$(getent passwd "$AGENT_USER" | cut -d: -f6) if [ -z "$AGENT_HOME" ] || [ ! -d "$AGENT_HOME" ]; then AGENT_HOME="/home/$AGENT_USER" fi CONFIG_DIR="${CLAUDE_CONFIG_DIR:-$AGENT_HOME/.claude}" mkdir -p "$CONFIG_DIR" chown "$AGENT_USER:" "$CONFIG_DIR" 2>/dev/null || true chmod 0700 "$CONFIG_DIR" # Build the .credentials.json file. The token is piped via stdin so it # never lands in argv (visible in /proc//cmdline) or env. The schema # matches what `claude setup-token` produces locally and what the legacy # SOPS path in harness_init.py wrote. CRED_FILE="$CONFIG_DIR/.credentials.json" cat "$ESO_OAUTH" | python3 -c " import json, sys token = sys.stdin.read().strip() print(json.dumps({ 'claudeAiOauth': { 'accessToken': token, 'refreshToken': None, 'expiresAt': 4102444800000, 'scopes': [ 'user:file_upload', 'user:inference', 'user:mcp_servers', 'user:profile', 'user:sessions:claude_code', ], 'subscriptionType': 'max', 'rateLimitTier': 'default_claude_max_5x', } })) " > "$CRED_FILE" chown "$AGENT_USER:" "$CRED_FILE" 2>/dev/null || true chmod 0600 "$CRED_FILE" echo "anthropic-cloud-paul-oauth: oauth_token staged into $CRED_FILE (0600 $AGENT_USER:$AGENT_USER)"