Tiered Claude API Architecture: Subscription UI + Budget-Conscious EC2 Farm-Out
This post documents the setup for a hybrid Claude deployment pattern: interactive use via claude.ai's subscription plan, with programmatic workloads automatically farmed out to a cost-optimized EC2 instance running cheaper model variants. The goal was to eliminate credential conflicts while maintaining a clean shell environment and preserving the ability to invoke either tier transparently.
The Problem: Credential Collision
The initial state had an ANTHROPIC_API_KEY export in ~/.zshrc, which created a conflict:
claudeCLI invocations would prefer the API key over the keychain OAuth token, forcing use of the cheaper API tier even for interactive subscription work- The claude.ai web login (managed via keychain credential at account
cbinClaude Code-credentials) was being bypassed - Environment-level key exports made it difficult to scope the key only where it was actually needed (remote farm-out calls)
The warning message was accurate but the solution space was misunderstood—the culprit wasn't settings.json configuration but shell environment precedence.
Architecture: Three-Tier Invocation
The solution establishes three distinct paths:
- Interactive subscription (`claude` from terminal): No API key in environment; Claude Code uses keychain OAuth token
- Programmatic local (scripts, automation): Source
repos.envto loadANTHROPIC_API_KEYonly in that subshell scope - Remote farm-out (hard problems → cheap EC2): SSH to Lightsail instance; pass model/key via daemon injection, not login shell
EC2 Infrastructure Details
The farm-out target is a Lightsail instance in us-west-2:
- Host:
ubuntu@34.239.233.28(public IP; internal hostnameip-172-26-6-34) - SSH key:
~/.ssh/LightsailDefaultKey-us-west-2.pem(passwordless, 4096-bit RSA) - Claude installation:
/usr/bin/claude(available; verified executable) - Auth mechanism:
jada-agent.service(systemd service, active/running)
The key infrastructure decision: the EC2 instance does not store credentials in the login shell or dotfiles. Instead, the agent daemon (jada-agent.service) injects model selection and API credentials per-invocation. This means farm-out wrappers must explicitly pass these values when SSH'ing remote commands.
Shell Environment Changes
The fix is surgical and non-destructive:
In ~/.zshrc: Remove the global export of ANTHROPIC_API_KEY. If it was unconditionally exported, delete or comment the line:
# OLD (problematic):
# export ANTHROPIC_API_KEY="sk-..."
# NEW: no global export
Keychain OAuth is already configured at account cb in the Claude Code-credentials item; once the API key is absent from the environment, Claude Code will automatically fall back to the stored OAuth token for interactive use.
For programmatic work, use explicit scoping:
# In scripts that need the API key:
source ~/.config/repos.env
# Now $ANTHROPIC_API_KEY is available in this subshell only
claude --model haiku "some task"
The repos.env file already contains the key (verified via grep); scripts that require it simply source it, following the existing convention for environment isolation.
Farm-Out Wrapper Pattern
For workloads delegated to EC2, the wrapper must handle SSH invocation plus credential passing. Example structure:
#!/bin/bash
# farm-out wrapper (pseudocode; exact path TBD)
REMOTE_USER="ubuntu"
REMOTE_HOST="34.239.233.28"
SSH_KEY="$HOME/.ssh/LightsailDefaultKey-us-west-2.pem"
REMOTE_CLAUDE="/usr/bin/claude"
# Source local repos.env to get API key
source "$HOME/.config/repos.env"
# SSH to remote, pass model and key via environment, invoke claude
ssh -i "$SSH_KEY" "$REMOTE_USER@$REMOTE_HOST" \
"ANTHROPIC_API_KEY='$ANTHROPIC_API_KEY' \
$REMOTE_CLAUDE --model haiku \"$@\""
Key design decision: The jada-agent.service daemon on EC2 is responsible for model selection and rate-limiting. The wrapper doesn't hard-code model choices on the remote side—it lets the daemon inject them. This centralizes cost control and prevents local overrides.
SSH verification: The connection was tested with BatchMode (no interactive prompts), confirming that the Lightsail key works passwordless and the remote claude binary is callable.
Credential Storage & Security
- Keychain (macOS): OAuth token for subscription plan remains in
Claude Code-credentials(accountcb). Never exported to shell; used only by Claude Code internally. repos.env: API key for programmatic work. Not exported globally; sourced only by scripts that need it. This file is local-only (not in version control).- EC2 daemon: The jada-agent.service manages injection per-call. SSH tunnels carry credentials only within the invocation scope; no long-lived exports on the remote.
This separation eliminates the risk of accidentally using the wrong tier—interactive CLI defaults to subscription (no key available), and programmatic/remote work explicitly opts in.
Testing & Validation
The following were verified read-only (no changes made during discovery):
- SSH connectivity to Lightsail instance using the LightsailDefaultKey pem file (BatchMode succeeded)
- Claude binary presence on remote (
/usr/bin/claude) - jada-agent.service status (active/running)
- Local keychain entry for OAuth token (Claude Code-credentials, account cb)
- repos.env contents and grep for ANTHROPIC_API_KEY references in local scripts
What's Next
Once ~/.zshrc is updated to remove the global API key export, the three-tier pattern becomes live. The next steps involve:
- Creating or updating the farm-out wrapper script (path TBD) to handle SSH + environment injection
- Testing interactive `claude` invocations to confirm keychain OAuth is used (no API key in env)
- Testing programmatic sources of `repos.env` to confirm API key scoping works
- Validating a sample farm-out call to EC2 (e.g., a haiku-model task) to confirm daemon injection and response