Bifurcating Claude API Authentication: Subscription UI for Interactive Work, Budget-Tier API for Farmed Tasks

The Problem: Authentication Conflict at Scale

When you have multiple systems consuming Claude—some interactive (CLI, Code), others programmatic (scripts, integrations)—a single global ANTHROPIC_API_KEY environment variable creates a hard choice: pay subscription rates for everything, or risk breaking production workflows with budget-tier models. This post documents how we resolved that conflict by bifurcating authentication: subscription credentials for interactive use, API-key-scoped credentials for farmed, decomposed tasks on a remote EC2 instance.

Architecture: Two Auth Paths, One Unified Workflow

The strategy is simple in principle, subtle in execution:

  • Interactive `claude` command: Uses OAuth keychain token (stored by Claude Code at ~/.claude/credentials, account `cb`), routed to claude.ai subscription plan.
  • Programmatic API calls: Source ~/repos.env (which exports ANTHROPIC_API_KEY) only in scoped contexts, hitting cheaper Haiku models on a remote daemon.
  • Farm-out wrapper: A shell function that detects decomposable tasks, SSHes to the EC2 box, and invokes Claude there with API credentials injected per-call.

The key insight: the authentication method is not a settings.json toggle—it's pure shell environment control. Remove the key from the global interactive environment, and the keychain token (already present) becomes the sole credential for the interactive shell.

Infrastructure: The EC2 Farm-Out Box

A dedicated Lightsail instance serves as the cheap-Claude executor:

  • Instance: ip-172-26-6-34 (public IP 34.239.233.28
  • SSH key: ~/.ssh/LightsailDefaultKey-us-west-2.pem (passwordless, BatchMode-compatible)
  • Claude daemon: /usr/bin/claude present; systemd unit jada-agent.service (active)
  • Auth mechanism: Credentials are not in the remote login shell; instead, they are injected by the daemon on a per-invocation basis via environment variables passed through SSH.

The critical detail: the remote Claude daemon does not read credentials from the remote user's shell environment. It expects them to arrive as arguments or environment variables at invocation time. This is why the farm-out wrapper must explicitly pass ANTHROPIC_API_KEY and model selection on each call.

Shell Configuration: Removing the Global API Key

The old approach was to export ANTHROPIC_API_KEY in ~/.zshrc, making it available globally. The new approach is:

# Old (before)
export ANTHROPIC_API_KEY="sk-ant-..."

# New (after): remove from ~/.zshrc entirely
# Instead, source only when needed:
if [[ -f ~/repos.env ]]; then
  source ~/repos.env  # Exports ANTHROPIC_API_KEY + other vars
fi

But we don't even source it in the interactive shell. We source it only in the farm-out wrapper function, which runs in a subshell:

claude-cheap() {
  # Decomposed, simple tasks → remote Haiku
  local task="$1"
  local host="ubuntu@34.239.233.28"
  local key="$HOME/.ssh/LightsailDefaultKey-us-west-2.pem"
  
  (
    source "$HOME/repos.env"
    ssh -i "$key" "$host" \
      env ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
      /usr/bin/claude --model claude-3-5-haiku-20241022 "$task"
  )
}

The interactive command claude itself remains unchanged—it continues to use the keychain token via Claude Code's built-in OAuth flow. No modifications to Claude Code settings are necessary.

Key Decisions and Rationale

Why Not `forceLoginMethod` in settings.json?

The forceLoginMethod key in ~/.claude/settings.json is enterprise/managed-instance only. In personal Claude Code installations, it is silently ignored. Attempting to set it would create a false sense of control. The actual lever is shell environment.

Why Inject Credentials via SSH Environment, Not in Remote .zshrc?

Putting the API key in the remote ~/.zshrc would be a vector for accidental exposure (e.g., if the EC2 instance is ever deprovisioned or shared). Injecting it at call time via env VAR=value ssh ... keeps it transient and auditable. It also allows rotating the key without touching the remote machine.

Why Haiku on EC2, Not GPT-4 or Claude 3.5 Sonnet?

Haiku is 90% cheaper than Sonnet for the same token consumption. For atomized, pre-decomposed tasks (e.g., "summarize this JSON", "lint this Python snippet"), Haiku's quality is sufficient. The subscription plan covers the high-touch reasoning work; the API budget tier handles the easy work.

Why a Farm-Out Wrapper, Not Direct API Calls?

A shell wrapper function makes the choice transparent to the caller. A script or interactive command can use the same interface (claude "task" or claude-cheap "task") without caring where it runs. This preserves composability and keeps the mental model simple.

Verification and Testing

Before deploying the change:

# Test: SSH to the box, verify Claude is running and reachable
ssh -i ~/.ssh/LightsailDefaultKey-us-west-2.pem ubuntu@34.239.233.28 \
  systemctl status jada-agent.service

# Output should show "active (running)"

# Test: Verify API key is NOT in interactive shell
echo $ANTHROPIC_API_KEY  # Should be empty

# Test: Verify keychain token is still available (tested by running `claude` normally)
claude "say hello"  # Should work and charge to subscription plan

# Test: Verify farm-out wrapper works
source ~/.zshrc
claude-cheap "parse this JSON: {\"key\": \"value\"}"

What's Next

  • Monitoring: Add CloudWatch alarms on the Lightsail instance to track EC2 API call volume and costs, ensuring the budget tier is actually cheaper as expected.
  • Graceful degradation: Implement a fallback that routes to subscription Claude if the EC2 box is unreachable, so interactive work is never blocked.
  • Key rotation: Document a procedure for rotating the API key stored in ~/repos.env without downtime (update locally, deploy via CI/CD to the EC2 instance if that becomes part of the workflow).
  • Cost tracking: Parse Claude API usage logs to confirm we've hit the 10x–20x cost reduction target.