Dual-Mode Claude Authentication: Separating Interactive Web UI from Programmatic API Key Usage
One of the persistent friction points in local development is managing multiple authentication contexts for the same tool. When you need Claude for interactive work (via claude.ai web UI billing) but also need to farm out cheaper programmatic workloads to a self-hosted instance, the shell environment becomes a battleground. This post covers the exact mechanism we used to separate these concerns cleanly, without destructive changes to credential storage.
The Problem: Token Collision in the Shell
The conflict arose because a global ANTHROPIC_API_KEY export in the shell dotfiles was interfering with Claude Code's ability to fall back to the keychain-stored OAuth token. When the API key exists in the environment, Claude Code prioritizes it over the OAuth credential—which bypassed the subscription billing plan and instead consumed API quota. For programmatic work that should use cheap Haiku models on the EC2 instance, this was the right behavior. For interactive use via claude login, it was wrong.
The initial instinct was to look for a forceLoginMethod setting in ~/.claude/settings.json, but that key is enterprise/managed-only and silently ignored in user configurations. The real lever is purely environmental, not configuration-file-based.
Authentication Paths: How Claude Code Chooses
Claude Code evaluates credentials in this order:
- Environment variable
ANTHROPIC_API_KEY— if present, use it directly for API calls - Keychain OAuth token — stored under account `cb` with service label `Claude Code-credentials`; this is the web UI subscription token
- No credential — prompt for login
The keychain token already exists and is untouched by this process. It was set during the initial claude login handshake and persists in macOS Keychain. The problem was that the global environment variable shadowed it.
The Solution: Environment-Scoped Key Isolation
Rather than delete the API key (which breaks farm-out scripts), we moved it from global shell scope to local scope—sourced only when needed:
- Remove the global export from
~/.zshrc
Delete or comment out any line likeexport ANTHROPIC_API_KEY=sk-...from the interactive shell initialization. - Keep the key in
repos.env
The key already lives in your project-specific environment file (repos.env, typically at the root of your development workspace). This file is sourced selectively by scripts, not by every shell session. - Verify farm-out scripts source the file
Any wrapper or daemon that invokes Claude programmatically must explicitly sourcerepos.envbefore calling Claude:#!/bin/bash source /path/to/repos.env /usr/bin/claude chat "your prompt"
With this setup:
- Interactive
claude loginand Claude Code → no API key in environment → falls back to keychain OAuth token → uses web UI subscription billing - Programmatic calls from farm-out wrapper → explicitly sources
repos.env→ API key is in scope → routes to API with cost control
Verified Infrastructure: The EC2 Farm-Out Box
Before finalizing the approach, we verified the actual farm-out destination was reachable and correctly configured:
| Host | ubuntu@34.239.233.28 (AWS Lightsail; internal IP ip-172-26-6-34) |
| SSH Key | ~/.ssh/LightsailDefaultKey-us-west-2.pem (passwordless, verified with BatchMode probe) |
| Claude Binary | /usr/bin/claude present and functional |
| Service | jada-agent.service (systemd) — active and running |
| Auth Pattern | API key and model injected per-invocation by daemon, not in login shell |
This means any farm-out wrapper must pass credentials as arguments or environment variables to the remote invocation, not rely on them being present in the EC2 box's shell profile.
Key Architectural Decisions
Why not use a separate shell profile for programmatic work?
A separate profile (e.g., ~/.zshrc-farm-out with the key exported) would work but introduces maintenance overhead and inconsistency. A single interactive profile that excludes the key, combined with explicit sourcing in scripts, is simpler and more transparent.
Why not store the API key in a separate secure location?
The key is already in repos.env, which is typically gitignored and stored outside version control. Moving it elsewhere doesn't improve security; it just adds a lookup step. The real security boundary is "don't export it globally into every shell session."
Why verify the EC2 connection before finalizing?
The farm-out approach is only viable if the destination actually exists and is reachable. A read-only probe (SSH with BatchMode yes) confirms the box is live, the key works, and the Claude binary is present without making any changes.
Implementation Checklist
- [ ] Open
~/.zshrcand locate any line exportingANTHROPIC_API_KEY - [ ] Comment out or remove that line
- [ ] Verify
repos.envcontains the API key - [ ] Update any farm-out wrapper or daemon to source
repos.envbefore invoking Claude - [ ] Test interactive
claude login(should not prompt for API key) - [ ] Test a programmatic call via the farm-out path (should succeed with the API key from
repos.env)
What's Next
With this separation in place, the next step is to integrate the farm-out mechanism into actual development workflows. This likely involves wr