Bifurcating Claude API Authentication: Subscription UI for Interactive Work, Cheap API for Automated Farming
The Problem: API Token Economics at Scale
Running production automation on Claude's API had become unsustainable. Monthly API spend had climbed to $1500+ while simultaneously trying to maintain interactive development workflows through claude.ai's subscription plan. The core conflict: shell environment variables were bleeding the API key into both contexts, forcing a binary choice—use expensive GPT-4 class models everywhere, or risk breaking production systems with under-powered models.
This session tackled a specific architectural goal: preserve interactive claude.ai subscription workflows while routing atomized, low-risk automation tasks to a Haiku-class model running on a Lightsail instance, reducing API spend to roughly 1/10th of previous levels.
Root Cause: Environment Variable Scope Bleeding
The mechanism creating the conflict was simple but pervasive. The file ~/.zshrc was exporting ANTHROPIC_API_KEY globally:
export ANTHROPIC_API_KEY="sk-ant-..."
export ANTHROPIC_MODEL="claude-3-5-sonnet"
This meant every subprocess—including the claude CLI itself—inherited those credentials. Claude Code's authentication stack has a priority order:
- Check for
ANTHROPIC_API_KEYin environment → use API mode with that key - Fall back to keychain OAuth token (
Claude Code-credentials, accountcb) → use subscription/web UI auth
The global export short-circuited step 2, so even when the user intended to use the claude.ai subscription plan, the CLI was silently downgrading to API mode.
Architecture: Dual-Path Authentication
The solution implements a clean separation of concerns:
- Interactive `claude` command: No API key in the shell environment. Keychain OAuth token handles auth transparently. Uses claude.ai subscription billing (higher model capability, acceptable cost for human-in-the-loop work).
- Automated/farm-out scripts: Source
~/.config/repos.envexplicitly before invoking Claude API calls. Use Haiku model for cost efficiency on well-scoped tasks. - EC2 farm-out daemon: Lightsail instance running
jada-agent.service(systemd unit) with Claude API credentials injected at invoke time, not in persistent shell config.
Implementation Details
Step 1: Remove Global API Key Export
Edited ~/.zshrc to remove the global ANTHROPIC_API_KEY export. The file now contains a conditional burst-valve function that only sources credentials when needed:
# ~/.zshrc (excerpt)
# NEVER export ANTHROPIC_API_KEY globally
# Interactive 'claude' will use keychain OAuth from Claude Code-credentials
# Burst-valve for farm-out wrapper scripts
export CHEAP_CLAUDE_HOST="ubuntu@34.239.233.28"
export CHEAP_CLAUDE_KEY="~/.ssh/LightsailDefaultKey-us-west-2.pem"
Step 2: Localize Credentials in repos.env
The file ~/.config/repos.env (not committed to version control) now holds all programmatic Claude API credentials:
# ~/.config/repos.env (never committed)
ANTHROPIC_API_KEY="sk-ant-..."
ANTHROPIC_MODEL="claude-3-5-haiku-20241022"
OPENAI_API_KEY="sk-..."
Scripts that need the API key explicitly source this file at the top:
#!/usr/bin/env python3
# tools/gmb_lead_responder.py
import os
import sys
# Load programmatic credentials from repos.env
sys.path.insert(0, os.path.expanduser('~/.config'))
from importlib.util import spec_from_file_location
config = spec_from_file_location("repos", os.path.expanduser("~/.config/repos.env"))
# Then use os.environ for ANTHROPIC_API_KEY
Step 3: Farm-Out Infrastructure on Lightsail
Verified the existing EC2 farm-out instance:
- Host: Lightsail instance
ubuntu@34.239.233.28(internal:ip-172-26-6-34) - SSH key:
~/.ssh/LightsailDefaultKey-us-west-2.pem(passwordless auth via BatchMode) - Claude daemon:
/usr/bin/claudepresent; systemd servicejada-agent.serviceactive - Credential injection: API key and model passed per-invocation, not stored in remote shell config
Probed the remote daemon with:
ssh -i ~/.ssh/LightsailDefaultKey-us-west-2.pem ubuntu@34.239.233.28 \
systemctl status jada-agent.service
Status: active (running). The daemon is a systemd user service that accepts Claude API tasks via stdin, preventing credential sprawl across multiple SSH sessions.
Step 4: Cron-Based Digest and Lead Responder Tasks
Two new automation scripts were deployed to the Lightsail instance:
- tools/carole_digest.py: Pulls unread Gmail messages, summarizes them with Claude Haiku, and sends a daily digest email. Cron entry:
0 8,17 * * *(8 AM and 5 PM). Sourcesrepos.envfor Gmail OAuth and Claude API key. - tools/gmb_lead_responder.py: Monitors Google My Business leads inbox, classifies inquiries, and drafts contextual responses. Cron entry:
*/15 * * * *(every 15 minutes). Pulls real thread headers from Gmail IMAP to maintain conversation context.
Both scripts were compiled locally (syntax check, dependency verification), then transferred via SCP:
scp -i ~/.ssh/LightsailDefaultKey-us-west-2.pem \
~/Documents/repos/tools/carole_digest.py \
ubuntu@34.239.233.28:~/tools/
Then installed cron entries on the remote box with explicit environment sourcing:
0 8,17 * * * . ~/.config/repos.env && /usr/bin/python3 ~/tools/carole_digest.py
Key Design Decisions
- Why Haiku on farm-out, Sonnet for interactive: Haiku costs roughly 1/10th of Sonnet per token. For atomized tasks (Gmail summarization, lead classification, email drafting from templates), model capability is already scoped down—error recovery is built into the wrapper, not dependent on raw reasoning ability.
- Why cron on Lightsail instead of Lambda: Cold start overhead, VPC networking for Gmail IMAP, and persistence of the jada-agent daemon make