diff --git a/src/query.ts b/src/query.ts index e458a71..40de154 100644 --- a/src/query.ts +++ b/src/query.ts @@ -223,6 +223,9 @@ export async function startClaudeQuery( permissionMode === "bypassPermissions" ) { options.allowDangerouslySkipPermissions = true; + if (typeof process.getuid === "function" && process.getuid() === 0) { + env.IS_SANDBOX = "1"; + } } const effort = trimmedString(params.effort); diff --git a/test/smoke.ts b/test/smoke.ts index 048b4cb..902bec7 100644 --- a/test/smoke.ts +++ b/test/smoke.ts @@ -55,6 +55,37 @@ async function main() { assert.equal(cleaned.KEEP, "1"); assert.equal(cleaned.PATH, "/usr/bin"); + // Root may only combine bypassPermissions with Claude's dangerous-skip flag + // when the child process is explicitly marked as sandboxed. + { + const { startClaudeQuery } = await import("../src/query.ts"); + let capturedOptions: Record | undefined; + const stream = { + async *[Symbol.asyncIterator]() {}, + }; + const handle = await startClaudeQuery({ + prompt: "test", + cwd: process.cwd(), + env: { PATH: "/usr/bin" }, + permissionMode: "bypassPermissions", + allowDangerouslySkipPermissions: true, + queryImpl: () => ({ options }) => { + capturedOptions = options as Record; + return stream; + }, + }); + const childEnv = capturedOptions?.env as + | Record + | undefined; + assert.equal( + childEnv?.IS_SANDBOX, + typeof process.getuid === "function" && process.getuid() === 0 + ? "1" + : undefined, + ); + handle.close(); + } + // The UI login relays the official CLI flow: its authorize URL comes back to // the host, and the code the user pastes goes into the CLI's stdin. {