Route Claude Code through a LiteLLM proxy with automatic API key management via 1Password CLI.
Use this when your team runs a shared LiteLLM gateway and stores API keys in 1Password. The wrapper handles config distribution, per-project key rotation, per-repo usage attribution, and keeps developer machines in sync — no manual env vars to manage.
macOS / Linux / WSL:
curl -fsSL https://raw.githubusercontent.com/aproorg/claude-wrapper/main/install.sh | bashWindows (PowerShell):
irm https://raw.githubusercontent.com/aproorg/claude-wrapper/main/install.ps1 | iex
The installer:
- Writes a process wrapper to
~/.local/bin/claudethat shadows the real binary - Adds
~/.local/binto your PATH (if not already there) - Prompts for your LiteLLM base URL and 1Password item reference
- Pre-fetches the team configuration
Non-interactive mode: When piped without a TTY (CI, Docker), prompts are skipped and defaults are used silently.
which claude # Should show ~/.local/bin/claude CLAUDE_DEBUG=1 claude # Shows resolved config
When testing a PR or unmerged branch, you must export CLAUDE_ENV_URL before running the installer. Inline VAR=val curl ... | bash doesn't work — env-var prefixes only set the var for the immediately-following command (curl), not for the bash subshell that runs the install script. Without the export, the installer silently falls back to main and downloads sibling files (the wrapper, claude-env.sh) from there.
export CLAUDE_ENV_URL="https://raw.githubusercontent.com/aproorg/claude-wrapper/<branch-name>/claude-env.sh" curl -fsSL "https://raw.githubusercontent.com/aproorg/claude-wrapper/<branch-name>/install.sh" | bash
Verify the install came from the branch by checking the [INFO] Source: line. The installer also patches the installed wrapper so subsequent claude launches keep fetching from that branch.
- Claude Code installed (Homebrew, npm, or standalone)
- 1Password CLI (
op) with CLI integration enabled - curl and git
This repo ships with defaults for the maintainers' infrastructure. To use it for your own org:
- Fork the repo and update
claude-env.sh:LITELLM_BASE_URL— your LiteLLM gateway URLOP_ACCOUNT— your 1Password team domainOP_ITEM— the 1Password vault path for your API keys
- Update the install URL in your fork's README to point to your raw GitHub URL
- Distribute — team members run your fork's one-liner to install
Individual developers can also override any value via ~/.config/claude/local.env without touching the shared config.
# Clear all caches (API keys + remote config) claude --clear-cache # Force refresh config cache only rm ~/.cache/claude/env-remote.sh # Re-run installer to update local settings curl -fsSL https://raw.githubusercontent.com/aproorg/claude-wrapper/main/install.sh | bash # Debug mode (shows resolved config + stale cache warnings) CLAUDE_DEBUG=1 claude
If you're working on this repo, use a symlink instead of the installed wrapper:
git clone <your-fork-url> ~/claude-wrapper mkdir -p ~/.local/bin ln -sf ~/claude-wrapper/claude ~/.local/bin/claude # Add to your .bashrc / .zshrc if ~/.local/bin isn't already on PATH: export PATH="$HOME/.local/bin:$PATH"
Verify: which claude should show ~/.local/bin/claude.
There's no automated test suite — verify the full install-to-launch path manually.
1. Clean slate
# Back up your current wrapper, then remove it along with all caches. # Use `cp -P` to preserve the symlink — plain `cp` dereferences it and # step 9's restore would replace your dev symlink with a static byte copy. cp -P ~/.local/bin/claude ~/.local/bin/claude.bak rm -f ~/.local/bin/claude rm -f ~/.cache/claude/env-remote.sh ~/.cache/claude/*.key # Confirm only the real binary remains which claude # should show /opt/homebrew/bin/claude (or wherever yours lives)
2. Install via curl
CLAUDE_FORCE=1 curl -fsSL https://raw.githubusercontent.com/aproorg/claude-wrapper/main/install.sh | bash3. Verify the wrapper shadows the real binary
which claude # must show ~/.local/bin/claude, NOT the real binary CLAUDE_DEBUG=1 claude --version # Expected: key=fetched (or key=cached), project name, base URL, model, then version
4. Test cache hit (no 1Password prompt)
CLAUDE_DEBUG=1 claude --version
# Expected: key=cached (no 1Password prompt on second run)5. Test empty-file guard
Run this inside the repo whose project key you want to clear (the command
derives the project name from the current git remote). Also unset
CLAUDE_PROJECT first — if it's set in your environment (e.g. via direnv
or a parent shell), it overrides the git-based detection and the empty-file
guard won't be exercised.
unset CLAUDE_PROJECT : > ~/.cache/claude/$(basename $(git remote get-url origin 2>/dev/null | sed 's/.*\///;s/\.git$//')).key CLAUDE_DEBUG=1 claude --version # Expected: key=fetched (empty cache file treated as miss)
6. Test cache re-fetch
rm ~/.cache/claude/env-remote.sh CLAUDE_DEBUG=1 claude --version # should re-fetch remote config
7. Test env var override
CLAUDE_MODEL="claude-sonnet-4-20250514" CLAUDE_DEBUG=1 claude --version # model line should show claude-sonnet-4-20250514
8. Test clear-cache
claude --clear-cache ls ~/.cache/claude/ # .key files and env-remote.sh should be gone CLAUDE_DEBUG=1 claude --version # should recover by re-fetching everything
9. Restore
# If you use the dev symlink: ln -sf ~/claude-wrapper/claude ~/.local/bin/claude # Or restore the backup: mv ~/.local/bin/claude.bak ~/.local/bin/claude
| Step | What to check |
|---|---|
| Install | Wrapper, local.env, and cached remote config all written |
which claude |
Resolves to ~/.local/bin/claude, not the real binary |
| Debug output | key=fetched or key=cached, correct project, base URL, model, and headers=x-github-repo: <project> |
| Cache hit | Second run shows key=cached, no 1Password prompt |
| Empty-file guard | Empty .key file treated as cache miss (key=fetched) |
| Cache re-fetch | Re-fetches after deleting env-remote.sh |
| Env var override | CLAUDE_MODEL takes priority over remote + local config |
| Clear cache | Removes all .key files and env-remote.sh |
| Recovery | Wrapper re-fetches everything and launches successfully |
How it works
The installer writes a process wrapper to ~/.local/bin/claude that shadows the real Claude Code binary. When you run claude, the wrapper:
- Finds the real binary (portable PATH iteration, skipping itself)
- Fetches the latest team config from this repo, validates it against an integrity check, caches it for 5 minutes, falls back to stale cache on network failure
- Sources the config to set
ANTHROPIC_BASE_URL,ANTHROPIC_AUTH_TOKEN, model defaults, per-project API keys, andANTHROPIC_CUSTOM_HEADERS(auto-injectsx-github-repo: <project>for LiteLLM per-repo attribution) - Optionally sources
~/.config/claude/middleware.shfor custom pre-launch hooks (errors are trapped with actionable messages) execs the real Claude Code with all original arguments
Config override chain: Remote defaults (claude-env.sh) → local overrides (~/.config/claude/local.env) → environment variables. Local values persist across remote config updates — the TTL-based refresh only re-fetches the remote config, never touching your local settings.
Per-project API keys
The wrapper detects the current project from the git remote (or directory name) and looks up a project-specific API key field in your 1Password item. If no project-specific field exists, it falls back to the default API Key field.
1Password item: "op://Vault/your-litellm-item"
├── API Key (default fallback)
├── backend-api (project-specific)
└── customer-portal (project-specific)
Override project detection: CLAUDE_PROJECT=my-project claude
Per-repo attribution (x-github-repo header)
Every request to the LiteLLM gateway is automatically tagged with an x-github-repo: <project> header so the gateway can attribute usage, spend, and rate-limits per repository — zero developer setup.
The value comes from the same project-detection logic used for API key lookup (CLAUDE_PROJECT), so a request from ~/code/your-org/backend-api is tagged x-github-repo: backend-api.
If you've already set ANTHROPIC_CUSTOM_HEADERS (via local.env, shell environment, or middleware.sh), the auto-injected header is appended on a new line so your custom headers are preserved:
x-team: platform
x-github-repo: backend-api
To suppress the header on a specific project (rare — e.g., experimental scratch work you don't want attributed), strip it in your personal ~/.config/claude/middleware.sh. The team config does not provide a built-in opt-out.
Configuration
| Variable | Default | Description |
|---|---|---|
CLAUDE_ENV_URL |
(this repo) | Override remote config URL |
CLAUDE_ENV_UPDATE_TTL |
300 |
Cache TTL in seconds |
CLAUDE_MODEL |
claude-opus-5 |
Override default model |
CLAUDE_PROJECT |
(auto-detected) | Override project name |
CLAUDE_DEBUG |
0 |
Enable debug output |
ANTHROPIC_CUSTOM_HEADERS |
(auto-set) | Auto-injected x-github-repo: $CLAUDE_PROJECT. Pre-existing values preserved (header appended on new line). |
CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS |
1 |
Suppress Claude Code's experimental beta headers and request fields. Set to 0 to lift the suppression. |
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS |
1 |
Enable the agent-teams feature. |
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC |
1 |
Suppress requests that aren't part of the session itself. |
CLAUDE_CODE_SUBAGENT_MODEL |
(unset) | Pin the model for all subagents. Left unset, a subagent uses the model its spawner requested, then its own definition's model, then the parent's. |
ENABLE_TOOL_SEARCH |
(unset) | Send MCP tool schemas on demand instead of inline. Accepts true, auto, or auto:N. Requires CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=0 — see Deferred MCP tool loading below. |
Files:
| File | Purpose |
|---|---|
~/.local/bin/claude |
Process wrapper (written by installer) |
~/.cache/claude/env-remote.sh |
Cached remote config (auto-refreshed) |
~/.config/claude/local.env |
Your local overrides (never auto-modified) |
~/.config/claude/middleware.sh |
Optional pre-launch hook |
Local config (local.env)
The installer stores your LiteLLM URL and 1Password item in ~/.config/claude/local.env. These values override the remote defaults and are never touched by automatic updates.
# View current settings cat ~/.config/claude/local.env # Re-run installer to change values (shows current as defaults) curl -fsSL https://raw.githubusercontent.com/aproorg/claude-wrapper/main/install.sh | bash
The file uses simple KEY="VALUE" format and has 0600 permissions.
Deferred MCP tool loading (tool search)
Claude Code can send MCP tool names up front and fetch individual schemas on demand via a ToolSearch tool, instead of inlining every schema in every request. It enables this automatically only when ANTHROPIC_BASE_URL is a first-party Anthropic host — sessions through the LiteLLM gateway fail that check, so it must be opted into explicitly.
The saving is large. Measured on this wrapper with four MCP servers connected, tool schemas were 86% of a trivial request's payload (~44K tokens); per-turn prompt tokens across a real session went from ~68,900 to ~20,700.
Opt in with both lines in local.env (~/.config/claude/local.env, or %APPDATA%\claude\local.env):
CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=0
ENABLE_TOOL_SEARCH=true
Both are required: tool search rides on the advanced-tool-use-2025年11月20日 beta, which the default suppression strips, so ENABLE_TOOL_SEARCH alone does nothing. Lifting the suppression also re-enables the other experimental betas — there is no per-beta switch. Besides true, the flag accepts auto and auto:N.
Verified against litellm.ai.apro.is on Bedrock: prompt-cache reads stayed monotonic with zero misses, tool_reference blocks round-tripped, and MCP tools resolved on demand. Works on opus-4-7/opus-4-8/opus-5 and sonnet-4-6/sonnet-5. haiku-4-5 rejects tools[].custom.defer_loading with HTTP 400 — so don't combine this with CLAUDE_CODE_SUBAGENT_MODEL pinned to haiku, or every subagent turn fails.
Middleware
Create ~/.config/claude/middleware.sh to run custom shell commands before Claude launches. The file is sourced (not executed), so it can export environment variables, modify PATH, or run setup commands.
# Example: add a custom env var echo 'export MY_CUSTOM_VAR="hello"' > ~/.config/claude/middleware.sh # Example: prepend to PATH echo 'export PATH="/opt/my-tools/bin:$PATH"' > ~/.config/claude/middleware.sh
- The file is optional — if missing, nothing happens.
- Errors in middleware are caught and reported with an actionable message (file path + fix instructions).
Windows (PowerShell)
irm https://raw.githubusercontent.com/aproorg/claude-wrapper/main/install.ps1 | iex
The installer:
- Downloads
claudestart.ps1to%LOCALAPPDATA%\Programs\claude-wrapper\ - Creates a
claudestart.cmdshim so it works fromcmd.exetoo - Adds the install directory to your user PATH
- Prompts for your LiteLLM base URL and 1Password item reference
Note: On Windows the command is
claudestart(notclaude) because Windows doesn't support the same binary-shadowing trick used on macOS/Linux.
Get-Command claudestart # Should show the .cmd shim $env:CLAUDE_DEBUG = "1"; claudestart # Shows resolved config
When testing a PR or unmerged branch, you must set $env:CLAUDE_ENV_URL to the branch's claude-env.sh URL before running the installer. Without it, the installer falls back to main and downloads sibling files (including claudestart.ps1) from there — which is almost certainly not what you want when testing a branch.
$base = "https://raw.githubusercontent.com/aproorg/claude-wrapper/<branch-name>" $env:CLAUDE_ENV_URL = "$base/claude-env.sh" $env:CLAUDE_FORCE = "1" # overwrite existing wrapper without backup irm "$base/install.ps1" | iex
Verify the install came from the branch by checking the [INFO] Source: line — it should reference your branch, not main. The installer also patches the installed claudestart.ps1 so subsequent launches keep fetching from that branch.
- Claude Code installed (npm global install or standalone)
- 1Password CLI (
op.exe) with CLI integration enabled - PowerShell 5.1+ (ships with Windows 10/11) or PowerShell 7+
- git (for project detection)
# Clear all caches (API keys + remote config) claudestart --clear-cache # Force refresh config cache only Remove-Item "$env:LOCALAPPDATA\claude\env-remote.sh" # Re-run installer to update local settings irm https://raw.githubusercontent.com/aproorg/claude-wrapper/main/install.ps1 | iex # Force reinstall wrapper (e.g. to pick up updates to claudestart.ps1) $env:CLAUDE_FORCE = "1"; irm https://raw.githubusercontent.com/aproorg/claude-wrapper/main/install.ps1 | iex # Debug mode $env:CLAUDE_DEBUG = "1"; claudestart
| File | Purpose |
|---|---|
%LOCALAPPDATA%\Programs\claude-wrapper\claudestart.ps1 |
PowerShell wrapper |
%LOCALAPPDATA%\Programs\claude-wrapper\claudestart.cmd |
CMD shim |
%LOCALAPPDATA%\claude\env-remote.sh |
Cached remote config |
%APPDATA%\claude\local.env |
Your local overrides |
%LOCALAPPDATA%\claude\<project>.key |
Cached API keys (12h TTL) |
If you prefer not to use the installer:
- Run
Set-ExecutionPolicy RemoteSignedin an elevated PowerShell (once) - Clone the repo and copy
claudestart.ps1to a directory on your PATH - Create a
claudestart.cmdnext to it with:@powershell -ExecutionPolicy Bypass -File "%~dp0claudestart.ps1" %*
Follow the macOS/Linux instructions inside your WSL shell — WSL uses the bash wrapper, not the PowerShell one.
Add to your settings.json:
{
"claudeCode.environmentVariables": [
{ "name": "CLAUDE_CODE_SKIP_AUTH_LOGIN", "value": "1" }
],
"claudeCode.claudeProcessWrapper": "claudestart.cmd"
}VSCode integration (macOS/Linux)
Add to your settings.json:
{
"claudeCode.environmentVariables": [
{ "name": "CLAUDE_CODE_SKIP_AUTH_LOGIN", "value": "1" }
],
"claudeCode.claudeProcessWrapper": "/Users/YOU/.local/bin/claude"
}