Skip to content
Draft
Show file tree
Hide file tree
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
12 changes: 10 additions & 2 deletions src/cli/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export function buildClaudeEnv(
};
setDefault("ANTHROPIC_BASE_URL", `http://127.0.0.1:${port}`);
const existingBaseUrl = env.ANTHROPIC_BASE_URL;
let targetsLocalProxy = false;
if (existingBaseUrl) {
try {
const parsed = new URL(existingBaseUrl);
Expand All @@ -101,6 +102,10 @@ export function buildClaudeEnv(
console.error(`⚠ Replacing stale opencodex ANTHROPIC_BASE_URL ${existingBaseUrl} with ${replacement}.`);
env.ANTHROPIC_BASE_URL = replacement;
}
const effective = new URL(env.ANTHROPIC_BASE_URL!);
targetsLocalProxy = effective.protocol === "http:"
&& (effective.hostname === "localhost" || effective.hostname === "127.0.0.1")
&& Number(effective.port) === port;
} catch {
// Preserve user-provided values that are not parseable URLs.
}
Expand All @@ -110,7 +115,10 @@ export function buildClaudeEnv(
// the user's Claude login. Only inject a token when the proxy actually requires an
// admission key; otherwise Claude Code keeps its own OAuth and sends it to us —
// native claude models then pass through verbatim (see server/claude-messages.ts).
if ((config.apiKeys?.length ?? 0) > 0) {
// Never pair a proxy admission secret with a user-selected destination. Also let an
// explicitly exported Anthropic API key remain the sole credential so Claude Code
// does not report conflicting authentication variables.
if (targetsLocalProxy && !env.ANTHROPIC_API_KEY && (config.apiKeys?.length ?? 0) > 0) {
setDefault("ANTHROPIC_AUTH_TOKEN", config.apiKeys![0].key);
}
// Detection reads the SANITIZED launch env — the exact object spawned below — so the
Expand All @@ -128,7 +136,7 @@ export function buildClaudeEnv(
env: () => env as NodeJS.ProcessEnv,
ownTokens: ownAdmissionTokens(config),
}));
if (!env.ANTHROPIC_AUTH_TOKEN && resolved.markerMode === "proxy") {
if (targetsLocalProxy && !env.ANTHROPIC_AUTH_TOKEN && !env.ANTHROPIC_API_KEY && resolved.markerMode === "proxy") {
env.ANTHROPIC_AUTH_TOKEN = PROXY_MARKER;
}
if (resolved.origin === "auto-unknown") {
Expand Down
20 changes: 20 additions & 0 deletions tests/claude-cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,26 @@ describe("ocx claude env assembly", () => {
expect(env.ANTHROPIC_AUTH_TOKEN).toBe("sk-ocx-123");
});

test("does not send the proxy admission key to a user-selected gateway", () => {
const env = buildClaudeEnv(cfg({
apiKeys: [{ id: "1", name: "main", key: "sk-ocx-123", createdAt: "2026-01-01" }],
}), 10100, {
ANTHROPIC_BASE_URL: "https://gateway.example.test",
}, {}, { preBunAnthropicSlots: ["ANTHROPIC_BASE_URL"] });
expect(env.ANTHROPIC_BASE_URL).toBe("https://gateway.example.test");
expect(env.ANTHROPIC_AUTH_TOKEN).toBeUndefined();
});

test("does not add an auth token when the user exported an Anthropic API key", () => {
const env = buildClaudeEnv(cfg({
apiKeys: [{ id: "1", name: "main", key: "sk-ocx-123", createdAt: "2026-01-01" }],
}), 10100, {
ANTHROPIC_API_KEY: "sk-ant-user",
}, {}, { preBunAnthropicSlots: ["ANTHROPIC_API_KEY"] });
expect(env.ANTHROPIC_API_KEY).toBe("sk-ant-user");
expect(env.ANTHROPIC_AUTH_TOKEN).toBeUndefined();
});

// Host-managed routing guard (devlog 260720_claude_authmode_persist/020):
// defends the spawn env against leftover cc-switch/CCR settings.json env hijack.
test("subscription mode leaves the host-managed auth assertion unset", () => {
Expand Down
Loading