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:

  • claude CLI 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 cb in Claude 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.env to load ANTHROPIC_API_KEY only 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 hostname ip-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 (account cb). 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