From 56bac8b91c49c096ec815f5c88b9b5f915cbdc2a Mon Sep 17 00:00:00 2001 From: vycdev2 Date: Sat, 15 Aug 2026 07:12:13 +0000 Subject: [PATCH] fix: sanitize Claude auth subprocess environment --- CHANGELOG.md | 2 +- src/claudeAuth.ts | 6 +++++- tests/claudeAuth.test.mjs | 32 +++++++++++++++++++++++++++++++ tests/fixtures/fakeClaudeAuth.mjs | 5 +++++ 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index deaf154..692b294 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ - Keep `!usage blocks` billing-window selection and displayed times in UTC regardless of the host timezone. - Preserve line indentation when MCP clients read saved message history or pending messages. - Match the generated MCP endpoint hostname to the server's IPv4 loopback listener. -- Strip inherited `CLAUDECODE` markers from Claude CLI subprocesses so Claudify can launch them from Claude Code-managed environments. +- Strip inherited `CLAUDECODE` markers from Claude CLI response and authentication subprocesses so Claudify can launch them from Claude Code-managed environments. - Reject required prompt values that contain no non-whitespace content. - Match legacy MCP history channel filters exactly instead of including similarly named channels. - Preserve URL-only embeds returned by the MCP `fetch-messages` tool. diff --git a/src/claudeAuth.ts b/src/claudeAuth.ts index 3c455d3..bfcae7a 100644 --- a/src/claudeAuth.ts +++ b/src/claudeAuth.ts @@ -190,7 +190,11 @@ export class ClaudeAuthManager { options.loginTimeoutMs, DEFAULT_LOGIN_TIMEOUT_MS, ); - this.env = options.env || process.env; + this.env = {}; + for (const [key, value] of Object.entries(options.env || process.env)) { + if (key.toUpperCase() === "CLAUDECODE") continue; + if (value !== undefined) this.env[key] = value; + } } async getStatus(): Promise { diff --git a/tests/claudeAuth.test.mjs b/tests/claudeAuth.test.mjs index 78d8e3e..2935898 100644 --- a/tests/claudeAuth.test.mjs +++ b/tests/claudeAuth.test.mjs @@ -80,6 +80,38 @@ test("preserves balanced delimiters within Claude login URLs", () => { assert.equal(extractClaudeLoginUrl(loginUrl), loginUrl); }); +test("does not pass nested Claude markers to auth subprocesses", async (t) => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "claudify-auth-")); + const markerPath = path.join(tempDir, "authenticated"); + const fakeCliPath = path.join(tempDir, "fake-claude.mjs"); + const fixturePath = path.join(currentDir, "fixtures", "fakeClaudeAuth.mjs"); + fs.copyFileSync(fixturePath, fakeCliPath); + fs.writeFileSync(markerPath, "authenticated", "utf8"); + t.after(() => fs.rmSync(tempDir, { recursive: true, force: true })); + + const manager = new ClaudeAuthManager({ + command: process.execPath, + prefixArgs: [fakeCliPath], + commandTimeoutMs: 2_000, + loginTimeoutMs: 2_000, + env: { + ...process.env, + CLAUDECODE: "1", + ClAuDeCoDe: "also-set", + CLAUDIFY_AUTH_TEST_MARKER: markerPath, + CLAUDIFY_AUTH_TEST_REQUIRE_TTY: "1", + }, + }); + + assert.equal((await manager.getStatus()).loggedIn, true); + fs.unlinkSync(markerPath); + assert.equal( + await manager.startLogin("owner"), + "https://claude.com/cai/oauth/authorize?test=1", + ); + manager.cancelLogin("owner"); +}); + test("runs a private login session and verifies its final status", async (t) => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "claudify-auth-")); const markerPath = path.join(tempDir, "authenticated"); diff --git a/tests/fixtures/fakeClaudeAuth.mjs b/tests/fixtures/fakeClaudeAuth.mjs index 472e712..1c869ac 100644 --- a/tests/fixtures/fakeClaudeAuth.mjs +++ b/tests/fixtures/fakeClaudeAuth.mjs @@ -3,6 +3,11 @@ import fs from "node:fs"; const [, , group, action] = process.argv; const markerPath = process.env.CLAUDIFY_AUTH_TEST_MARKER; +if (Object.keys(process.env).some((key) => key.toUpperCase() === "CLAUDECODE")) { + process.stderr.write("Nested Claude sessions are not supported.\n"); + process.exit(1); +} + if (!markerPath || group !== "auth") { process.exit(2); }