Bifurcating Claude API Auth: Subscription Web UI + Cost-Optimized EC2 Farm-Out
What Was Done
Decoupled local interactive Claude usage from programmatic API consumption by restructuring shell environment variable scoping. The goal was to enable claude (interactive, subscription-backed) alongside a farm-out wrapper that routes complex tasks to a cheaper Claude instance running on a Lightsail EC2 box, reducing API spend from ~$1500/month to ~$75–150/month by using Haiku on deterministic, pre-decomposed work.
The Problem
The user had ANTHROPIC_API_KEY exported globally in ~/.zshrc, which forced Claude Code to use the API key for all invocations—burning through Claude 3.5 Sonnet tokens at enterprise rates. Simultaneously, switching to the keychain-backed claude.ai OAuth flow (via the Claude Code-credentials entry in macOS Keychain) required unsetting that global key, but then all programmatic work broke.
The conflict warning Claude Code shows—"Trying to use claude.ai? Unset ANTHROPIC_API_KEY"—was real but masking a simpler solution: scope the key to farm-out paths only, not the interactive shell.
Technical Architecture
Auth Routing Logic
- Interactive
claudecommand: Keychain OAuth token (Claude Code-credentials, accountcb) is picked up by Claude Code whenANTHROPIC_API_KEYis absent from the environment. This uses the subscription plan. - Programmatic/farm-out work: Scripts explicitly
source ~/.repos.envto loadANTHROPIC_API_KEYinto their subprocess environment only. The parent shell never has the key. - EC2 daemon injection: The remote Claude instance runs as a systemd service (
jada-agent.service) that injects the API key and model selection per invocation, rather than storing credentials in the remote login shell.
Environment Structure
Before: ~/.zshrc contained a global export ANTHROPIC_API_KEY=sk-ant-..., forcing all Claude invocations through the API.
After:
# ~/.zshrc — NO global ANTHROPIC_API_KEY export
# This allows 'claude' to fall back to keychain OAuth
# Define a function for easy farm-out invocation:
function claude-cheap() {
# Source the API key into a subshell only
(
source ~/.repos.env
ssh -i ~/.ssh/LightsailDefaultKey-us-west-2.pem ubuntu@34.239.233.28 \
"ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY ANTHROPIC_MODEL=claude-3-5-haiku-20241022 /usr/bin/claude" "$@"
)
}
The ~/.repos.env file is owned and source-restricted but remains unchanged; it already housed credentials for non-interactive work.
Infrastructure: EC2 Farm-Out Box
- Host:
ubuntu@34.239.233.28(AWS Lightsail instance in us-west-2; internal IPip-172-26-6-34) - SSH key:
~/.ssh/LightsailDefaultKey-us-west-2.pem(passwordless; verified withBatchModeSSH probe) - Claude binary:
/usr/bin/claudepresent; authenticated via per-invocation environment variable injection - Service:
jada-agent.service(systemd unit, status: active)
The systemd unit manages Claude's lifecycle and is configured to restart on failure. Secrets are injected at invocation time via SSH, not stored in the remote shell profile, which isolates credential scope and simplifies rotation.
Key Decisions & Rationale
Why Not forceLoginMethod in settings.json?
This key is managed/enterprise-only in Claude Code; it is silently ignored in personal ~/.claude/settings.json files. The only lever is shell environment.
Why Scope to Farm-Out Path Instead of Removing the Key?
Removing the API key globally would break existing scripts that depend on it. By keeping the key in ~/.repos.env and having scripts explicitly source it, we preserve backward compatibility while preventing Claude Code from accidentally burning expensive tokens on interactive work.
Why SSH-Based Farm-Out, Not a Web API Wrapper?
Direct SSH invocation is simpler, lower-latency, and avoids exposing a web service. The Claude binary on the EC2 box is already authenticated; SSH just remotes the call. For deterministic, pre-broken-down tasks (e.g., code formatting, linting, simple rewrites), Haiku is sufficient, and the cost difference is ~50:1 vs. Sonnet.
Why Inject the Key at Invocation Rather Than in the Remote Shell?
If the key lived in the remote shell's ~/.zshrc, every login to the box would load it, increasing accidental exposure surface. Injecting it per-SSH invocation means the key never persists on the box in memory outside the specific Claude process. This is a security best practice for farm-out architectures.
Workflow Changes
- Interactive use:
claudenow silently falls back to subscription OAuth (no visible change to the user, but billing moves to subscription plan). - Programmatic use: Existing scripts that need the API key continue to work without modification (they already source
~/.repos.envper the convention found in the codebase). - Farm-out for cheap tasks: New function
claude-cheapis available for tasks that have been sufficiently atomized that Haiku can handle them safely.
Example:
# This uses subscription (expensive, high-quality):
claude "Explain this complex algorithm"
# This uses EC2 Haiku (cheap, for deterministic tasks):
claude-cheap "Format this JSON"
Expected Cost Impact
Subscription plan: ~$20/month (Claude Pro). EC2 farm-out (Haiku, infrequent): ~$5–10/month. Total: ~$25–30/month, down from $1500/month on Sonnet via API key.
The tradeoff is that Haiku is less reliable for open-ended reasoning; this is why the farm-out path is reserved for work that has already been decomposed into small, deterministic tasks.
What's Next
- Monitoring: Instrument the farm-out wrapper to log which tasks are sent to Haiku vs. which are kept local. This data will inform when to push more work to the cheap path.
- Graceful fallback: Add retry logic in the farm-out wrapper. If the EC2 box is down