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
27 changes: 1 addition & 26 deletions src/cli/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ export type ClaudeEnvDeps = {
authDetect?: Omit<Partial<AuthDetectDeps>, "env" | "ownTokens">;
/** Test seam; production uses the authenticated Node-launcher context. */
preBunAnthropicSlots?: readonly AnthropicParentEnvSlot[] | null;
/** Explicit unsafe opt-in from a root `--dangerously-skip-permissions` launch. */
allowRootSkipPermissions?: boolean;
};

function isClaudeLoopbackHostname(hostname: string): boolean {
Expand Down Expand Up @@ -117,9 +115,6 @@ export function buildClaudeEnv(
if (env[name] !== undefined && env[name] !== "") return; // user wins
env[name] = value;
};
if (deps.allowRootSkipPermissions === true) {
setDefault("IS_SANDBOX", "1");
}
setDefault("ANTHROPIC_BASE_URL", `http://127.0.0.1:${port}`);
const existingBaseUrl = env.ANTHROPIC_BASE_URL;
if (existingBaseUrl) {
Expand Down Expand Up @@ -306,22 +301,6 @@ export function claudeNotFoundHint(
return platform === "win32" && code === 9009 && !signal ? CLAUDE_INSTALL_HINT : null;
}

export function shouldAllowRootSkipPermissions(
args: readonly string[],
getuid: (() => number) | null | undefined = process.getuid,
): boolean {
return args.includes("--dangerously-skip-permissions")
&& typeof getuid === "function"
&& getuid() === 0;
}

export function rootSkipPermissionsNotice(env: ClaudeLaunchEnv): string {
if (env.IS_SANDBOX === "1") {
return "⚠ Root --dangerously-skip-permissions requested: OpenCodex set IS_SANDBOX=1 to bypass Claude Code's root guard. OpenCodex did not create an OS sandbox; prefer running as a non-root user.";
}
return `⚠ Root --dangerously-skip-permissions requested: preserving user IS_SANDBOX=${env.IS_SANDBOX}; Claude Code's root guard remains in control.`;
}

export async function cmdClaude(args: string[]): Promise<number> {
const config = loadConfig();
if (config.claudeCode?.enabled === false) {
Expand All @@ -334,11 +313,7 @@ export async function cmdClaude(args: string[]): Promise<number> {
return 1;
}
const contextWindows = await fetchClaudeContextWindows(config, port);
const allowRootSkipPermissions = shouldAllowRootSkipPermissions(args);
const env = buildClaudeEnv(config, port, process.env, contextWindows, { allowRootSkipPermissions });
if (allowRootSkipPermissions) {
console.error(rootSkipPermissionsNotice(env));
}
const env = buildClaudeEnv(config, port, process.env, contextWindows);
// Pre-write the CLI's gateway-model cache (devlog 030): without a token the CLI
// never refreshes it, so the picker would keep showing yesterday's aliases.
try {
Expand Down
37 changes: 4 additions & 33 deletions tests/claude-cli.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
import { buildClaudeEnv, claudeNotFoundHint, rootSkipPermissionsNotice, shouldAllowRootSkipPermissions } from "../src/cli/claude";
import { buildClaudeEnv, claudeNotFoundHint } from "../src/cli/claude";
import { commandInvocation } from "../src/lib/win-exec";
import type { OcxConfig } from "../src/types";

Expand All @@ -26,38 +26,9 @@ const AUTH_PRESENT = {
};

describe("ocx claude env assembly", () => {
test("root skip-permissions bypass requires both the explicit flag and uid 0", () => {
expect(shouldAllowRootSkipPermissions(["--dangerously-skip-permissions"], () => 0)).toBe(true);
expect(shouldAllowRootSkipPermissions([], () => 0)).toBe(false);
expect(shouldAllowRootSkipPermissions(["--dangerously-skip-permissions"], () => 1000)).toBe(false);
expect(shouldAllowRootSkipPermissions(["--dangerously-skip-permissions"], null)).toBe(false);
});

test("root skip-permissions opt-in marks only that launch as sandboxed", () => {
const bypass = buildClaudeEnv(cfg(), 10100, {}, {}, {
...AUTH_PRESENT,
allowRootSkipPermissions: true,
});
expect(bypass.IS_SANDBOX).toBe("1");

const ordinary = buildClaudeEnv(cfg(), 10100, {}, {}, AUTH_PRESENT);
expect(ordinary.IS_SANDBOX).toBeUndefined();
});

test("an explicit user sandbox value wins over the root skip-permissions opt-in", () => {
const env = buildClaudeEnv(cfg(), 10100, { IS_SANDBOX: "0" }, {}, {
...AUTH_PRESENT,
allowRootSkipPermissions: true,
});
expect(env.IS_SANDBOX).toBe("0");
expect(rootSkipPermissionsNotice(env)).toContain("preserving user IS_SANDBOX=0");
expect(rootSkipPermissionsNotice(env)).toContain("root guard remains in control");
});

test("the unsafe root bypass notice discloses that no OS sandbox was created", () => {
const notice = rootSkipPermissionsNotice({ IS_SANDBOX: "1" });
expect(notice).toContain("set IS_SANDBOX=1");
expect(notice).toContain("did not create an OS sandbox");
test("does not assert a sandbox that OpenCodex did not create", () => {
expect(buildClaudeEnv(cfg(), 10100, {}, {}, AUTH_PRESENT).IS_SANDBOX).toBeUndefined();
expect(buildClaudeEnv(cfg(), 10100, { IS_SANDBOX: "0" }, {}, AUTH_PRESENT).IS_SANDBOX).toBe("0");
});

test("injects base URL, discovery flag and model slots — NO auth token by default (subscription mode)", () => {
Expand Down
Loading