Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion src/ucode/agents/claude.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import signal
import socket
import subprocess
import sys
import threading
from pathlib import Path
from typing import cast
Expand All @@ -32,7 +33,7 @@
from ucode.state import mark_tool_managed, save_state
from ucode.telemetry import agent_version, ucode_version
from ucode.tracing import tracing_env
from ucode.ui import print_note, print_success, print_warning
from ucode.ui import print_err, print_note, print_success, print_warning

CLAUDE_CONFIG_DIR = Path.home() / ".claude"
CLAUDE_SETTINGS_PATH = CLAUDE_CONFIG_DIR / "ucode-settings.json"
Expand Down Expand Up @@ -118,6 +119,29 @@ def _resolve_web_search_model(state: dict) -> str | None:
_RELAYED_SETTING_SOURCES = "project,local"


def _managed_settings_path() -> Path | None:
"""OS-specific location of Claude Code's enterprise managed-settings.json.
Returns None on unsupported platforms."""
if sys.platform.startswith("linux"):
return Path("/etc/claude-code/managed-settings.json")
if sys.platform == "darwin":
return Path("/Library/Application Support/ClaudeCode/managed-settings.json")
return None


def _managed_api_key_helper_path() -> Path | None:
"""Path to the enterprise managed-settings.json when it sets an apiKeyHelper,
else None. The managed scope always wins over --settings and subscription
OAuth, so a managed apiKeyHelper silently shadows relayed auth."""
path = _managed_settings_path()
if path is None or not path.is_file():
return None
settings = read_json_safe(path)
if isinstance(settings, dict) and settings.get("apiKeyHelper"):
return path
return None


def relayed_proxy_base_url(state: dict) -> str:
"""Loopback base URL for the relayed refresh proxy, allocating a free port
on first call and caching it in state so config and launch agree."""
Expand Down Expand Up @@ -776,6 +800,17 @@ def _launch_relayed(state: dict, binary: str, tool_args: list[str]) -> None:
exec, so we spawn-and-wait rather than replacing the process)."""
from ucode.gateway_proxy import start_proxy

managed_path = _managed_api_key_helper_path()
if managed_path is not None:
print_err(
f"Enterprise managed settings ({managed_path}) set an 'apiKeyHelper', "
"which Claude Code always applies over relayed (Claude Max/Enterprise) "
"auth — you'd be billed for API usage instead of using your subscription. "
"Remove the 'apiKeyHelper' from that file (or ask your admin to) before "
"running relayed auth. Not starting Claude Code."
)
raise SystemExit(1)

_ensure_subscription_login()
workspace = state["workspace"]
port = state.get("relayed_proxy_port")
Expand Down
Loading