Bifurcating Claude API Access: Subscription UI for Interactive Work, Cost-Optimized API for Programmatic Tasks
The Problem: Cost Spiral and Auth Conflicts
When you're running systems that depend on Claude API across multiple environments—interactive development, automated scripts, CI/CD pipelines—you face a hard constraint: a single global API key in your shell environment forces everything through the same billing tier. At $1500/month in token spend, that's unsustainable. Meanwhile, interactive development work (which is higher-touch, lower-volume) should use the Claude subscription web UI—cheaper per-interaction, user-friendly, and already paid for. The tension is real: you can't afford to run all API calls through the subscription plan, but you also can't let low-stakes programmatic work break high-stakes interactive sessions by burning through quota.
The secondary issue: claude.ai (subscription token via keychain OAuth) and ANTHROPIC_API_KEY (API billing) both exist in your environment, and Claude Code can't cleanly switch between them when the key is globally exported in ~/.zshrc.
Technical Diagnosis
The solution isn't in Claude Code settings (the forceLoginMethod key is enterprise-only and silently ignored in ~/.claude/settings.json). It's purely shell-environment scoping.
Current state:
ANTHROPIC_API_KEYexported globally in~/.zshrc(sourced from~/.config/repos.env)- Claude Code keychain OAuth token stored at
~/.claude/credentials/Claude Code-credentials(accountcb), already configured and untouched - EC2 Lightsail box (
ubuntu@34.239.233.28, internal IPip-172-26-6-34) withclaudeCLI installed andjada-agent.serviceactive - SSH access via
~/.ssh/LightsailDefaultKey-us-west-2.pem(passwordless, verified withBatchModeprobe) - The EC2 Claude daemon injects API credentials per-invocation, not in the login shell
Architecture: Dual-Path Invocation
The fix creates two distinct code paths, each optimized for its use case:
Path 1: Interactive `claude` → Subscription Web UI
When you type claude in an interactive shell, it invokes the claude.ai web UI (or Claude Code) using your keychain OAuth token. The shell environment must not have ANTHROPIC_API_KEY set at invocation time. Once the global export is removed from ~/.zshrc, Claude Code's credential chain falls back to the keychain OAuth token stored at ~/.claude/credentials/Claude Code-credentials.
Path 2: Programmatic & Farm-Out → API Key + Cheapest Model
Scripts, CI/CD, and farm-out wrappers that need the API key source ~/.config/repos.env explicitly. This keeps the key scoped to subprocess environments only, never polluting the global interactive shell. Farm-out work (broken into granular, low-risk tasks) runs on the EC2 box via the jada-agent.service daemon, which injects the API key and routes to the cheapest viable Claude model (e.g., Haiku for well-scoped subtasks).
Implementation Details
1. Remove Global API Key Export
In ~/.zshrc, delete or comment out:
export ANTHROPIC_API_KEY=$(cat ~/.config/repos.env | grep ANTHROPIC_API_KEY | cut -d'=' -f2)
Why: This forces interactive shells to rely on Claude Code's keychain OAuth, which is already configured and cheaper per-interaction.
2. Establish the Farm-Out Wrapper Convention
Any script that needs the API key must explicitly source ~/.config/repos.env at the start:
#!/bin/bash
set -e
# Load API credentials for this subprocess only
source ~/.config/repos.env
# Now $ANTHROPIC_API_KEY is set; child processes inherit it
# Invoke cheap Claude on EC2 for a granular subtask
ssh -i ~/.ssh/LightsailDefaultKey-us-west-2.pem ubuntu@34.239.233.28 \
/usr/local/bin/claude-cheap-invoke --model haiku --task "parse_json_output"
Why: Subprocess scoping avoids polluting the parent shell. The EC2 box then handles routing to the optimal (cheapest, fastest-failing) model for that task.
3. EC2 Daemon: jada-agent.service
The Lightsail box already runs an active systemd service at /etc/systemd/system/jada-agent.service. This daemon:
- Listens for SSH invocations from your workstation
- Accepts task metadata (model, prompt, task ID)
- Injects
ANTHROPIC_API_KEYfrom secure storage (e.g., an encrypted env file or systemd secret) before each Claude invocation - Routes to the cheapest model that can safely handle the task
- Returns results via stdout/stderr or S3 (for large outputs)
Key file paths on the EC2 box:
/usr/bin/claude— Claude CLI binary (confirmed present)/etc/systemd/system/jada-agent.service— Service unit (active and verified)/var/log/jada-agent.log— Service logs (for debugging farm-out failures)~ubuntu/.config/repos.env— API key and model preferences (injected by the service, not the login shell)
4. SSH & Batching
Farm-out invocations use SSH in BatchMode (no interactive prompts), with the Lightsail key:
ssh -i ~/.ssh/LightsailDefaultKey-us-west-2.pem \
-o BatchMode=yes \
-o StrictHostKeyChecking=accept-new \
ubuntu@34.239.233.28 \
/usr/local/bin/invoke-claude-task --model haiku --input "$TASK_JSON"
This ensures that high-latency network calls don't block your interactive session; farm-out work is fire-and-forget or polled asynchronously.
Key Decisions & Trade-Offs
- Why not use `forceLoginMethod` in settings.json? — It's enterprise/managed-Claude only; silently ignored in personal Claude Code. The shell-environment approach is the only reliable lever.
- Why source `~/.config/repos.env` in scripts instead of exporting it globally? — Subprocess scoping keeps the API key out of your interactive prompt history and out of child processes that don't need it (reducing blast radius if a