Decoupling Claude API Auth: Tiered CLI Access for Cost-Optimized LLM Workflows
What Was Done
We restructured shell environment variable scoping to enable two distinct Claude access patterns from a single developer workstation:
- Interactive CLI (`claude` command): Routes through claude.ai subscription billing via keychain OAuth token
- Programmatic/API workloads: Farm out to a cost-optimized Claude instance (Haiku) running on EC2, with API key scoped only to farm-out wrapper scripts
This eliminates the auth conflict that occurs when ANTHROPIC_API_KEY is globally exported—it forces all invocations (including interactive ones) toward API billing, preventing subscription token usage even when available. The fix is purely shell-environment-driven; no settings.json modifications are needed (the forceLoginMethod key is enterprise-managed and silently ignored in user configs).
Technical Details: The Auth Conflict
The root issue: Claude Code prioritizes auth sources in a specific order:
- Environment variable
ANTHROPIC_API_KEY - Keychain credential (stored as account
cbunder serviceClaude Code-credentials) - Interactive login flow
When ANTHROPIC_API_KEY is globally exported in ~/.zshrc, every invocation—whether interactive or scripted—uses that key. This prevents the keychain OAuth token from ever being consulted, making subscription billing inaccessible from the CLI even when you want it.
The solution: never export the API key globally. Instead, keep it in a file that is sourced only when needed.
Infrastructure: EC2 Farm-Out Instance
The target farm-out box is a Lightsail instance with these properties:
- Host:
ubuntu@34.239.233.28(public IP); internal hostnameip-172-26-6-34 - Region: us-west-2
- SSH Key:
~/.ssh/LightsailDefaultKey-us-west-2.pem(passwordless; batch mode verified) - Claude Installation:
/usr/bin/claude(present and functional) - Service:
jada-agent.service(systemd; active state verified)
Critical detail: the EC2 instance does not have the API key in its login shell environment. Instead, the jada-agent.service daemon injects API credentials per-invocation. This means farm-out wrapper scripts must pass the key explicitly when shelling into the instance.
Implementation: Shell Environment Restructuring
Step 1: Remove global API key export from interactive shell
In ~/.zshrc, remove or comment out any line like:
export ANTHROPIC_API_KEY="sk-..." # DELETE THIS LINE
This ensures that interactive claude invocations have no API key in the environment, forcing them to consult the keychain and use your subscription OAuth token.
Step 2: Scope API key to farm-out context only
Keep the API key in repos.env (your existing convention for script-scoped config):
# ~/.repos.env
export ANTHROPIC_API_KEY="sk-..."
export ANTHROPIC_MODEL="claude-3-5-haiku-20241022"
Scripts that need API access source this file before invoking farm-out logic:
#!/bin/bash
# farm-out wrapper example
source ~/.repos.env
ssh -i ~/.ssh/LightsailDefaultKey-us-west-2.pem ubuntu@34.239.233.28 \
"ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY ANTHROPIC_MODEL=$ANTHROPIC_MODEL /usr/bin/claude $@"
Step 3: Preserve keychain auth for interactive use
The keychain credential (service Claude Code-credentials, account cb) remains untouched. Once the API key is removed from the interactive environment, the CLI automatically falls back to this token on first use.
Key Decisions & Rationale
Why not use forceLoginMethod in settings.json?
- This key is enterprise/managed-organization only. In personal configs, Claude silently ignores it.
- The only reliable lever for personal deployments is shell-environment state.
Why scope the key to repos.env instead of a separate secrets file?
- Consistency with existing project convention (scripts already source this file).
- Reduces the number of credential files to audit and rotate.
- Clear intent: if a script needs API access, it must explicitly declare it via
source.
Why pass the key via SSH environment variables rather than storing it on the EC2 box?
- Minimizes credential sprawl: no second copy of the key stored on a remote machine.
- Simplifies rotation: update
repos.envlocally, and all farm-out invocations automatically use the new key on next run. - Reduces blast radius: if the EC2 box is ever compromised, the attacker does not have long-lived credential storage.
Why Haiku for farm-out vs. Opus?
- Cost: Haiku is ~6–10x cheaper per token than Opus.
- Atomized tasks: farm-out is scoped to already-broken-down, low-ambiguity problems (data transformation, simple formatting, boilerplate generation) where Haiku's lower reasoning capability is acceptable.
- Subscription tier (claude.ai) is still used for complex analysis and decision-making at the workstation; EC2 handles only the commodity work.
Verification & Testing
Before moving to production:
- SSH connectivity: Test passwordless key auth to the Lightsail box:
ssh -i ~/.ssh/LightsailDefaultKey-us-west-2.pem ubuntu@34.239.233.28 "echo ok" - Claude on EC2: Verify the daemon is running:
ssh -i ~/.ssh/LightsailDefaultKey-us-west-2.pem ubuntu@34.239.233.28 "systemctl status jada-agent.service"