feat(cli): add litellm-proxy run -- <agent> to wrap coding agents through the proxy

Wraps Claude Code, Codex, OpenCode, and any other coding agent so all of its
LLM traffic routes through a LiteLLM proxy, with the agent-vault style of "just
works" DX: one `run -- <agent>` command, auto SSO login when interactive,
env-key "agent mode" for containers/CI, and a fail-fast key check against the
proxy so bad credentials error immediately instead of deep inside the agent.

The wrapped binary is detected by name to pick the right variables. Claude Code
gets ANTHROPIC_BASE_URL (the bare proxy root, so it appends /v1/messages) and
ANTHROPIC_AUTH_TOKEN, with any stray ANTHROPIC_API_KEY cleared so the proxy
token wins. Codex and OpenCode get OPENAI_BASE_URL (proxy + /v1) and
OPENAI_API_KEY. Unrecognized commands get both sets so they work either way.
`litellm-proxy claude-code` remains as a shortcut for `run -- claude`.

The core logic is split into dependency-injected helpers (agent_profile,
build_agent_env, verify_proxy_key, run_agent) so env wiring, the preflight, and
the launch handoff are unit-tested without monkeypatching, alongside CliRunner
tests for auth resolution, agent mode, and auto-login. Mutation-tested the env
profiles, preflight, and agent-mode branch to confirm the tests fail when the
behavior is broken.

https://claude.ai/code/session_0154VpLXW7mMvk5wfbgPRJa6
This commit is contained in:
mateo-berri 2026-06-06 18:54:36 +00:00
parent 51769a8ede
commit 1670b9b4c0
No known key found for this signature in database
5 changed files with 672 additions and 0 deletions

View file

@ -438,6 +438,36 @@ litellm-proxy http request POST /chat/completions -j '{"model": "gpt-4", "messag
litellm-proxy http request GET /health/test_connection -H "X-Custom-Header:value"
```
### Run a Coding Agent
Run a coding agent (Claude Code, Codex, OpenCode, and others) with all of its LLM traffic routed through your LiteLLM proxy. Everything after `--` is the command to launch:
```bash
litellm-proxy run -- claude
litellm-proxy run -- codex
litellm-proxy run -- opencode
```
`litellm-proxy claude-code` is a shortcut for `litellm-proxy run -- claude`.
The wrapper resolves your LiteLLM key (logging in via SSO when none is stored and you are at a terminal; otherwise it expects `LITELLM_PROXY_API_KEY` or `--api-key`), checks the key against the proxy so bad credentials fail immediately instead of deep inside the agent, exports the environment variables the agent reads, then replaces itself with the agent process.
The command is detected from the binary name to pick the right variables. Claude Code gets `ANTHROPIC_BASE_URL` (the proxy root, so it appends `/v1/messages`) and `ANTHROPIC_AUTH_TOKEN`, with any stray `ANTHROPIC_API_KEY` cleared so the proxy token wins. Codex and OpenCode get `OPENAI_BASE_URL` (the proxy plus `/v1`) and `OPENAI_API_KEY`. An unrecognized command gets both sets so it works either way.
Options:
- `--model`, `-m`: Model Claude Code should request (sets `ANTHROPIC_MODEL`); must resolve on your proxy. OpenAI-style agents take the model through their own flag.
- `--small-fast-model`: Background-task model for Claude Code (sets `ANTHROPIC_SMALL_FAST_MODEL`).
- `--skip-verify`: Skip the pre-launch key check (useful offline or with non-standard auth).
Forward flags the agent owns after `--`:
```bash
litellm-proxy run --model claude-sonnet-proxy -- claude --resume
```
Whatever model the agent requests (default Claude names, or your `--model` override) must exist on the proxy, since requests land on the proxy's `/v1/messages` (Anthropic) or `/v1/chat/completions` and `/v1/responses` (OpenAI) endpoints.
## Environment Variables
The CLI respects the following environment variables:

View file

@ -0,0 +1,263 @@
import os
import shutil
import sys
from typing import Callable, Dict, FrozenSet, Mapping, Optional, Sequence, Tuple
import click
import requests
from .auth import get_stored_api_key, login
ANTHROPIC_BASE_URL_ENV = "ANTHROPIC_BASE_URL"
ANTHROPIC_AUTH_TOKEN_ENV = "ANTHROPIC_AUTH_TOKEN"
ANTHROPIC_API_KEY_ENV = "ANTHROPIC_API_KEY"
ANTHROPIC_MODEL_ENV = "ANTHROPIC_MODEL"
ANTHROPIC_SMALL_FAST_MODEL_ENV = "ANTHROPIC_SMALL_FAST_MODEL"
OPENAI_BASE_URL_ENV = "OPENAI_BASE_URL"
OPENAI_API_KEY_ENV = "OPENAI_API_KEY"
PROFILE_ANTHROPIC = "anthropic"
PROFILE_OPENAI = "openai"
_KNOWN_AGENTS: Dict[str, Tuple[str, FrozenSet[str]]] = {
"claude": ("Claude Code", frozenset({PROFILE_ANTHROPIC})),
"codex": ("Codex", frozenset({PROFILE_OPENAI})),
"opencode": ("OpenCode", frozenset({PROFILE_OPENAI})),
}
_INSTALL_DOCS: Dict[str, str] = {
"claude": "https://docs.claude.com/en/docs/claude-code/setup",
"codex": "https://developers.openai.com/codex/cli",
"opencode": "https://opencode.ai/docs",
}
class AgentRunError(Exception):
"""Raised for any user-actionable failure while preparing to run an agent."""
def agent_profile(command: str) -> Tuple[str, FrozenSet[str]]:
"""Return the (display name, env profiles) for a wrapped command.
Known agents map to the API family they speak. Anything else gets both
families so it works regardless of which env vars the tool reads.
"""
base = os.path.basename(command)
if base in _KNOWN_AGENTS:
return _KNOWN_AGENTS[base]
return base, frozenset({PROFILE_ANTHROPIC, PROFILE_OPENAI})
def build_agent_env(
base_env: Mapping[str, str],
base_url: str,
api_key: str,
profiles: FrozenSet[str],
*,
model: Optional[str] = None,
small_fast_model: Optional[str] = None,
) -> Dict[str, str]:
"""Return a copy of base_env wired to route the agent through the proxy.
Anthropic clients (Claude Code) append /v1/messages to ANTHROPIC_BASE_URL,
so it stays the bare proxy root; OpenAI clients (Codex, OpenCode) expect the
/v1 suffix on OPENAI_BASE_URL. ANTHROPIC_API_KEY is dropped so a stray
Anthropic key cannot win over the bearer token we set.
"""
env = dict(base_env)
root = base_url.rstrip("/")
if PROFILE_ANTHROPIC in profiles:
env[ANTHROPIC_BASE_URL_ENV] = root
env[ANTHROPIC_AUTH_TOKEN_ENV] = api_key
env.pop(ANTHROPIC_API_KEY_ENV, None)
if model:
env[ANTHROPIC_MODEL_ENV] = model
if small_fast_model:
env[ANTHROPIC_SMALL_FAST_MODEL_ENV] = small_fast_model
if PROFILE_OPENAI in profiles:
env[OPENAI_BASE_URL_ENV] = root + "/v1"
env[OPENAI_API_KEY_ENV] = api_key
return env
def verify_proxy_key(
base_url: str,
api_key: str,
*,
get: Callable[..., requests.Response] = requests.get,
) -> None:
"""Probe the proxy with the key so bad creds fail here, not inside the agent.
Raises AgentRunError when the proxy is unreachable or rejects the key. Other
non-2xx responses are tolerated; the agent's own call is the real test.
"""
url = base_url.rstrip("/") + "/v1/models"
try:
resp = get(url, headers={"Authorization": f"Bearer {api_key}"}, timeout=10)
except requests.RequestException as e:
raise AgentRunError(
f"Could not reach the LiteLLM proxy at {base_url.rstrip('/')}: {e}. "
"Is it running, and is --base-url (or LITELLM_PROXY_URL) correct?"
)
if resp.status_code in (401, 403):
raise AgentRunError(
f"LiteLLM rejected your key (HTTP {resp.status_code}). "
"Run `litellm-proxy login` to refresh it, or pass a valid --api-key."
)
def _exec(path: str, args: Sequence[str], env: Mapping[str, str]) -> None:
os.execvpe(path, list(args), dict(env))
def run_agent(
base_url: str,
api_key: str,
command: Sequence[str],
*,
model: Optional[str] = None,
small_fast_model: Optional[str] = None,
skip_verify: bool = False,
base_env: Optional[Mapping[str, str]] = None,
which: Callable[[str], Optional[str]] = shutil.which,
verify: Callable[[str, str], None] = verify_proxy_key,
launcher: Callable[[str, Sequence[str], Mapping[str, str]], None] = _exec,
) -> None:
"""Validate, wire the environment, and hand off to the agent.
On success this replaces the current process and never returns. Raises
AgentRunError for missing binaries, an unreachable proxy, or a rejected key.
"""
if not command:
raise AgentRunError("Nothing to run. Try `litellm-proxy run -- claude`.")
_, profiles = agent_profile(command[0])
binary = which(command[0])
if binary is None:
docs = _INSTALL_DOCS.get(os.path.basename(command[0]))
hint = f" Install it first: {docs}" if docs else ""
raise AgentRunError(f"Could not find `{command[0]}` on your PATH.{hint}")
if not skip_verify:
verify(base_url, api_key)
env = build_agent_env(
base_env if base_env is not None else os.environ,
base_url,
api_key,
profiles,
model=model,
small_fast_model=small_fast_model,
)
launcher(binary, command, env)
def _is_interactive() -> bool:
return sys.stdin.isatty()
def _resolve_api_key(ctx: click.Context) -> str:
base_url = ctx.obj["base_url"]
api_key = ctx.obj.get("api_key")
if api_key:
return api_key
if not _is_interactive():
raise click.ClickException(
"No LiteLLM key found. Set LITELLM_PROXY_API_KEY (or pass --api-key) for "
"non-interactive use, or run `litellm-proxy login` from a terminal."
)
click.echo("No LiteLLM credentials found; starting login...")
ctx.invoke(login)
api_key = get_stored_api_key(expected_base_url=base_url)
if not api_key:
raise click.ClickException(
"Login did not produce an API key; cannot start the agent."
)
return api_key
_MODEL_HELP = (
"Model the agent should request, exported as ANTHROPIC_MODEL for Claude Code. "
"Must resolve on your proxy. OpenAI-style agents take the model via their own flag."
)
_SMALL_FAST_MODEL_HELP = (
"Background-task model for Claude Code, exported as ANTHROPIC_SMALL_FAST_MODEL."
)
_SKIP_VERIFY_HELP = "Skip the pre-launch key check against the proxy."
@click.command(name="run", context_settings={"ignore_unknown_options": True})
@click.option("--model", "-m", default=None, help=_MODEL_HELP)
@click.option("--small-fast-model", default=None, help=_SMALL_FAST_MODEL_HELP)
@click.option("--skip-verify", is_flag=True, default=False, help=_SKIP_VERIFY_HELP)
@click.argument("command", nargs=-1, type=click.UNPROCESSED, required=True)
@click.pass_context
def run(
ctx: click.Context,
model: Optional[str],
small_fast_model: Optional[str],
skip_verify: bool,
command: Sequence[str],
):
"""Run a coding agent wired to your LiteLLM proxy.
Everything after `--` is the command to execute, e.g.
`litellm-proxy run -- claude`, `litellm-proxy run -- codex`, or
`litellm-proxy run -- opencode`. Logs in with LiteLLM if needed, checks the
key against the proxy, exports the env vars the agent reads, then hands off.
"""
command = list(command)
base_url = ctx.obj["base_url"]
api_key = _resolve_api_key(ctx)
display_name, _ = agent_profile(command[0])
click.echo(
f"litellm: routing {display_name} through proxy at {base_url.rstrip('/')}"
)
try:
run_agent(
base_url,
api_key,
command,
model=model,
small_fast_model=small_fast_model,
skip_verify=skip_verify,
)
except AgentRunError as e:
raise click.ClickException(str(e))
@click.command(name="claude-code", context_settings={"ignore_unknown_options": True})
@click.option("--model", "-m", default=None, help=_MODEL_HELP)
@click.option("--small-fast-model", default=None, help=_SMALL_FAST_MODEL_HELP)
@click.option("--skip-verify", is_flag=True, default=False, help=_SKIP_VERIFY_HELP)
@click.argument("claude_args", nargs=-1, type=click.UNPROCESSED)
@click.pass_context
def claude_code(
ctx: click.Context,
model: Optional[str],
small_fast_model: Optional[str],
skip_verify: bool,
claude_args: Sequence[str],
):
"""Shortcut for `litellm-proxy run -- claude`. Extra args are forwarded to claude."""
ctx.invoke(
run,
model=model,
small_fast_model=small_fast_model,
skip_verify=skip_verify,
command=("claude", *claude_args),
)
__all__ = [
"run",
"claude_code",
"run_agent",
"build_agent_env",
"verify_proxy_key",
"agent_profile",
]

View file

@ -91,6 +91,8 @@ def show_commands():
("keys", "Manage API keys"),
("teams", "Manage teams and team assignments"),
("users", "Manage users"),
("run", "Run a coding agent (claude, codex, ...) through the proxy"),
("claude-code", "Shortcut for `run -- claude`"),
("version", "Show version information"),
("help", "Show this help message"),
("quit", "Exit the interactive session"),

View file

@ -15,6 +15,7 @@ from .commands.keys import keys
# local imports
from .commands.models import models
from .commands.run import claude_code, run
from .commands.teams import teams
from .commands.users import users
from .interface import interactive_shell
@ -112,6 +113,9 @@ cli.add_command(keys)
cli.add_command(teams)
# Add the users command group
cli.add_command(users)
# Add the agent runner commands
cli.add_command(run)
cli.add_command(claude_code)
if __name__ == "__main__":

View file

@ -0,0 +1,373 @@
import os
import sys
from unittest.mock import patch
import click
import pytest
import requests
from click.testing import CliRunner
sys.path.insert(
0, os.path.abspath("../../..")
) # Adds the parent directory to the system path
from litellm.proxy.client.cli.commands.run import (
AgentRunError,
agent_profile,
build_agent_env,
claude_code,
run,
run_agent,
verify_proxy_key,
)
RUN_MODULE = "litellm.proxy.client.cli.commands.run"
class _FakeResponse:
def __init__(self, status_code):
self.status_code = status_code
class TestAgentProfile:
def test_claude_is_anthropic(self):
name, profiles = agent_profile("claude")
assert name == "Claude Code"
assert profiles == frozenset({"anthropic"})
def test_claude_full_path_uses_basename(self):
name, profiles = agent_profile("/usr/local/bin/claude")
assert name == "Claude Code"
assert profiles == frozenset({"anthropic"})
def test_codex_and_opencode_are_openai(self):
assert agent_profile("codex") == ("Codex", frozenset({"openai"}))
assert agent_profile("opencode") == ("OpenCode", frozenset({"openai"}))
def test_unknown_command_gets_both_profiles(self):
name, profiles = agent_profile("mytool")
assert name == "mytool"
assert profiles == frozenset({"anthropic", "openai"})
class TestBuildAgentEnv:
def test_anthropic_profile_uses_bare_root_and_bearer(self):
env = build_agent_env(
{}, "http://localhost:4000/", "sk-key", frozenset({"anthropic"})
)
assert env["ANTHROPIC_BASE_URL"] == "http://localhost:4000"
assert env["ANTHROPIC_AUTH_TOKEN"] == "sk-key"
assert "OPENAI_BASE_URL" not in env
assert "OPENAI_API_KEY" not in env
def test_anthropic_profile_drops_existing_api_key(self):
env = build_agent_env(
{"ANTHROPIC_API_KEY": "real-key"},
"http://localhost:4000",
"sk-key",
frozenset({"anthropic"}),
)
assert "ANTHROPIC_API_KEY" not in env
def test_openai_profile_appends_v1(self):
env = build_agent_env(
{}, "http://localhost:4000/", "sk-key", frozenset({"openai"})
)
assert env["OPENAI_BASE_URL"] == "http://localhost:4000/v1"
assert env["OPENAI_API_KEY"] == "sk-key"
assert "ANTHROPIC_BASE_URL" not in env
def test_both_profiles_set_everything(self):
env = build_agent_env(
{}, "http://localhost:4000", "sk-key", frozenset({"anthropic", "openai"})
)
assert env["ANTHROPIC_BASE_URL"] == "http://localhost:4000"
assert env["OPENAI_BASE_URL"] == "http://localhost:4000/v1"
assert env["ANTHROPIC_AUTH_TOKEN"] == "sk-key"
assert env["OPENAI_API_KEY"] == "sk-key"
def test_model_overrides_only_apply_to_anthropic(self):
anthropic = build_agent_env(
{},
"http://localhost:4000",
"sk-key",
frozenset({"anthropic"}),
model="claude-proxy",
small_fast_model="haiku-proxy",
)
assert anthropic["ANTHROPIC_MODEL"] == "claude-proxy"
assert anthropic["ANTHROPIC_SMALL_FAST_MODEL"] == "haiku-proxy"
openai = build_agent_env(
{},
"http://localhost:4000",
"sk-key",
frozenset({"openai"}),
model="claude-proxy",
)
assert "ANTHROPIC_MODEL" not in openai
def test_model_overrides_omitted_when_not_given(self):
env = build_agent_env(
{}, "http://localhost:4000", "sk-key", frozenset({"anthropic"})
)
assert "ANTHROPIC_MODEL" not in env
assert "ANTHROPIC_SMALL_FAST_MODEL" not in env
def test_preserves_unrelated_env_and_does_not_mutate_input(self):
base = {"PATH": "/usr/bin", "ANTHROPIC_API_KEY": "real-key"}
env = build_agent_env(
base, "http://localhost:4000", "sk-key", frozenset({"anthropic"})
)
assert env["PATH"] == "/usr/bin"
assert base == {"PATH": "/usr/bin", "ANTHROPIC_API_KEY": "real-key"}
class TestVerifyProxyKey:
def test_ok_status_passes_and_uses_models_endpoint(self):
captured = {}
def fake_get(url, headers, timeout):
captured["url"] = url
captured["headers"] = headers
return _FakeResponse(200)
verify_proxy_key("http://localhost:4000/", "sk-key", get=fake_get)
assert captured["url"] == "http://localhost:4000/v1/models"
assert captured["headers"] == {"Authorization": "Bearer sk-key"}
@pytest.mark.parametrize("status", [401, 403])
def test_rejected_key_raises(self, status):
with pytest.raises(AgentRunError, match="rejected your key"):
verify_proxy_key(
"http://localhost:4000",
"sk-key",
get=lambda *a, **k: _FakeResponse(status),
)
def test_unreachable_proxy_raises(self):
def boom(*a, **k):
raise requests.ConnectionError("refused")
with pytest.raises(AgentRunError, match="Could not reach"):
verify_proxy_key("http://localhost:4000", "sk-key", get=boom)
def test_other_non_2xx_is_tolerated(self):
verify_proxy_key(
"http://localhost:4000",
"sk-key",
get=lambda *a, **k: _FakeResponse(500),
)
class TestRunAgent:
def test_wires_env_and_launches_resolved_binary(self):
calls = {}
def fake_launcher(path, args, env):
calls["path"] = path
calls["args"] = tuple(args)
calls["env"] = dict(env)
run_agent(
"http://localhost:4000",
"sk-key",
["claude", "--resume"],
base_env={"PATH": "/usr/bin", "ANTHROPIC_API_KEY": "leaked"},
which=lambda name: "/usr/local/bin/claude",
verify=lambda *a: None,
launcher=fake_launcher,
)
assert calls["path"] == "/usr/local/bin/claude"
assert calls["args"] == ("claude", "--resume")
env = calls["env"]
assert env["ANTHROPIC_BASE_URL"] == "http://localhost:4000"
assert env["ANTHROPIC_AUTH_TOKEN"] == "sk-key"
assert "ANTHROPIC_API_KEY" not in env
assert "OPENAI_BASE_URL" not in env
def test_codex_gets_openai_env(self):
calls = {}
run_agent(
"http://localhost:4000",
"sk-key",
["codex"],
base_env={},
which=lambda name: "/usr/local/bin/codex",
verify=lambda *a: None,
launcher=lambda p, a, e: calls.update(env=dict(e)),
)
assert calls["env"]["OPENAI_BASE_URL"] == "http://localhost:4000/v1"
assert calls["env"]["OPENAI_API_KEY"] == "sk-key"
assert "ANTHROPIC_BASE_URL" not in calls["env"]
def test_missing_binary_raises_with_install_hint(self):
with pytest.raises(AgentRunError, match="claude.*Install it first"):
run_agent(
"http://localhost:4000",
"sk-key",
["claude"],
base_env={},
which=lambda name: None,
verify=lambda *a: None,
launcher=lambda *a: None,
)
def test_skip_verify_does_not_call_verify(self):
verified = []
launched = []
run_agent(
"http://localhost:4000",
"sk-key",
["claude"],
skip_verify=True,
base_env={},
which=lambda name: "/usr/local/bin/claude",
verify=lambda *a: verified.append(a),
launcher=lambda *a: launched.append(a),
)
assert verified == []
assert len(launched) == 1
def test_verify_failure_aborts_before_launch(self):
launched = []
def boom(*a):
raise AgentRunError("rejected")
with pytest.raises(AgentRunError):
run_agent(
"http://localhost:4000",
"sk-key",
["claude"],
base_env={},
which=lambda name: "/usr/local/bin/claude",
verify=boom,
launcher=lambda *a: launched.append(a),
)
assert launched == []
def test_empty_command_raises(self):
with pytest.raises(AgentRunError):
run_agent("http://localhost:4000", "sk-key", [])
class TestRunCommand:
def setup_method(self):
self.runner = CliRunner()
def test_launches_with_stored_key_and_forwards_args(self):
captured = {}
def fake_run_agent(base_url, api_key, command, **kwargs):
captured["base_url"] = base_url
captured["api_key"] = api_key
captured["command"] = list(command)
captured["model"] = kwargs.get("model")
with patch(f"{RUN_MODULE}.run_agent", side_effect=fake_run_agent):
result = self.runner.invoke(
run,
["--model", "claude-proxy", "--", "claude", "--resume"],
obj={"base_url": "http://localhost:4000", "api_key": "sk-key"},
)
assert result.exit_code == 0, result.output
assert captured["api_key"] == "sk-key"
assert captured["command"] == ["claude", "--resume"]
assert captured["model"] == "claude-proxy"
assert (
"routing Claude Code through proxy at http://localhost:4000"
in result.output
)
def test_codex_shows_friendly_name(self):
with patch(f"{RUN_MODULE}.run_agent"):
result = self.runner.invoke(
run,
["--", "codex"],
obj={"base_url": "http://localhost:4000", "api_key": "sk-key"},
)
assert result.exit_code == 0, result.output
assert "routing Codex through proxy" in result.output
def test_non_interactive_without_key_errors_clearly(self):
with (
patch(f"{RUN_MODULE}._is_interactive", return_value=False),
patch(f"{RUN_MODULE}.run_agent") as mock_run,
):
result = self.runner.invoke(
run,
["--", "claude"],
obj={"base_url": "http://localhost:4000", "api_key": None},
)
assert result.exit_code != 0
assert "LITELLM_PROXY_API_KEY" in result.output
mock_run.assert_not_called()
def test_interactive_without_key_logs_in_then_launches(self):
captured = {}
@click.command()
def fake_login():
pass
with (
patch(f"{RUN_MODULE}._is_interactive", return_value=True),
patch(f"{RUN_MODULE}.login", fake_login),
patch(
f"{RUN_MODULE}.get_stored_api_key", return_value="sk-after-login"
) as mock_get,
patch(
f"{RUN_MODULE}.run_agent",
side_effect=lambda base_url, api_key, command, **k: captured.update(
api_key=api_key
),
),
):
result = self.runner.invoke(
run,
["--", "claude"],
obj={"base_url": "http://localhost:4000", "api_key": None},
)
assert result.exit_code == 0, result.output
assert captured["api_key"] == "sk-after-login"
mock_get.assert_called_once_with(expected_base_url="http://localhost:4000")
def test_agent_run_error_becomes_click_error(self):
with patch(
f"{RUN_MODULE}.run_agent",
side_effect=AgentRunError("could not reach proxy"),
):
result = self.runner.invoke(
run,
["--", "claude"],
obj={"base_url": "http://localhost:4000", "api_key": "sk-key"},
)
assert result.exit_code != 0
assert "could not reach proxy" in result.output
class TestClaudeCodeShortcut:
def setup_method(self):
self.runner = CliRunner()
def test_delegates_to_run_with_claude_prepended(self):
captured = {}
def fake_run_agent(base_url, api_key, command, **kwargs):
captured["command"] = list(command)
with patch(f"{RUN_MODULE}.run_agent", side_effect=fake_run_agent):
result = self.runner.invoke(
claude_code,
["--", "--resume"],
obj={"base_url": "http://localhost:4000", "api_key": "sk-key"},
)
assert result.exit_code == 0, result.output
assert captured["command"] == ["claude", "--resume"]