From 449fa524f0ddff4e8bd87b6d9b325147d5f8e163 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 13 Mar 2026 07:52:11 -0500 Subject: [PATCH 1/3] feat: inject CLAUDE_CODE_OAUTH_TOKEN into sandbox environment Add CLAUDE_CODE_OAUTH_TOKEN to getSandboxEnv() following the same optional pattern as GITHUB_TOKEN. This flows through to both codingAgentTask and updatePRTask via runClaudeCodeAgent(). Co-Authored-By: Claude Opus 4.6 --- src/sandboxes/__tests__/getSandboxEnv.test.ts | 16 +++++++++++----- src/sandboxes/getSandboxEnv.ts | 5 +++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/sandboxes/__tests__/getSandboxEnv.test.ts b/src/sandboxes/__tests__/getSandboxEnv.test.ts index 1942b29..2edc73d 100644 --- a/src/sandboxes/__tests__/getSandboxEnv.test.ts +++ b/src/sandboxes/__tests__/getSandboxEnv.test.ts @@ -7,19 +7,21 @@ describe("getSandboxEnv", () => { beforeEach(() => { process.env.RECOUP_API_KEY = "test-api-key"; process.env.GITHUB_TOKEN = "test-github-token"; + process.env.CLAUDE_CODE_OAUTH_TOKEN = "test-oauth-token"; }); afterEach(() => { process.env = { ...originalEnv }; }); - it("returns RECOUP_API_KEY, RECOUP_ACCOUNT_ID, and GITHUB_TOKEN", () => { + it("returns RECOUP_API_KEY, RECOUP_ACCOUNT_ID, GITHUB_TOKEN, and CLAUDE_CODE_OAUTH_TOKEN", () => { const env = getSandboxEnv("acc_123"); expect(env).toEqual({ RECOUP_API_KEY: "test-api-key", RECOUP_ACCOUNT_ID: "acc_123", GITHUB_TOKEN: "test-github-token", + CLAUDE_CODE_OAUTH_TOKEN: "test-oauth-token", }); }); @@ -28,13 +30,17 @@ describe("getSandboxEnv", () => { const env = getSandboxEnv("acc_123"); - expect(env).toEqual({ - RECOUP_API_KEY: "test-api-key", - RECOUP_ACCOUNT_ID: "acc_123", - }); expect(env).not.toHaveProperty("GITHUB_TOKEN"); }); + it("omits CLAUDE_CODE_OAUTH_TOKEN when not set", () => { + delete process.env.CLAUDE_CODE_OAUTH_TOKEN; + + const env = getSandboxEnv("acc_123"); + + expect(env).not.toHaveProperty("CLAUDE_CODE_OAUTH_TOKEN"); + }); + it("throws when RECOUP_API_KEY is missing", () => { delete process.env.RECOUP_API_KEY; diff --git a/src/sandboxes/getSandboxEnv.ts b/src/sandboxes/getSandboxEnv.ts index 3906254..e847e64 100644 --- a/src/sandboxes/getSandboxEnv.ts +++ b/src/sandboxes/getSandboxEnv.ts @@ -20,5 +20,10 @@ export function getSandboxEnv( env.GITHUB_TOKEN = githubToken; } + const claudeCodeOauthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN; + if (claudeCodeOauthToken) { + env.CLAUDE_CODE_OAUTH_TOKEN = claudeCodeOauthToken; + } + return env; } From 8883e228ea5e544c62a858d42e351fc4bad41736 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 13 Mar 2026 07:59:30 -0500 Subject: [PATCH 2/3] feat: inject CLAUDE_CODE_OAUTH_TOKEN directly in runClaudeCodeAgent Read CLAUDE_CODE_OAUTH_TOKEN from process.env inside runClaudeCodeAgent and merge it into the command env. This ensures every claude CLI invocation gets the token without prop-drilling through all callers. Reverts the getSandboxEnv approach which only covered updatePRTask. Co-Authored-By: Claude Opus 4.6 --- src/sandboxes/__tests__/getSandboxEnv.test.ts | 16 ++-- .../__tests__/runClaudeCodeAgent.test.ts | 81 ++++++++++++++++++- src/sandboxes/getSandboxEnv.ts | 5 -- src/sandboxes/runClaudeCodeAgent.ts | 10 ++- 4 files changed, 93 insertions(+), 19 deletions(-) diff --git a/src/sandboxes/__tests__/getSandboxEnv.test.ts b/src/sandboxes/__tests__/getSandboxEnv.test.ts index 2edc73d..1942b29 100644 --- a/src/sandboxes/__tests__/getSandboxEnv.test.ts +++ b/src/sandboxes/__tests__/getSandboxEnv.test.ts @@ -7,21 +7,19 @@ describe("getSandboxEnv", () => { beforeEach(() => { process.env.RECOUP_API_KEY = "test-api-key"; process.env.GITHUB_TOKEN = "test-github-token"; - process.env.CLAUDE_CODE_OAUTH_TOKEN = "test-oauth-token"; }); afterEach(() => { process.env = { ...originalEnv }; }); - it("returns RECOUP_API_KEY, RECOUP_ACCOUNT_ID, GITHUB_TOKEN, and CLAUDE_CODE_OAUTH_TOKEN", () => { + it("returns RECOUP_API_KEY, RECOUP_ACCOUNT_ID, and GITHUB_TOKEN", () => { const env = getSandboxEnv("acc_123"); expect(env).toEqual({ RECOUP_API_KEY: "test-api-key", RECOUP_ACCOUNT_ID: "acc_123", GITHUB_TOKEN: "test-github-token", - CLAUDE_CODE_OAUTH_TOKEN: "test-oauth-token", }); }); @@ -30,17 +28,13 @@ describe("getSandboxEnv", () => { const env = getSandboxEnv("acc_123"); + expect(env).toEqual({ + RECOUP_API_KEY: "test-api-key", + RECOUP_ACCOUNT_ID: "acc_123", + }); expect(env).not.toHaveProperty("GITHUB_TOKEN"); }); - it("omits CLAUDE_CODE_OAUTH_TOKEN when not set", () => { - delete process.env.CLAUDE_CODE_OAUTH_TOKEN; - - const env = getSandboxEnv("acc_123"); - - expect(env).not.toHaveProperty("CLAUDE_CODE_OAUTH_TOKEN"); - }); - it("throws when RECOUP_API_KEY is missing", () => { delete process.env.RECOUP_API_KEY; diff --git a/src/sandboxes/__tests__/runClaudeCodeAgent.test.ts b/src/sandboxes/__tests__/runClaudeCodeAgent.test.ts index 08bc0a8..e9e43aa 100644 --- a/src/sandboxes/__tests__/runClaudeCodeAgent.test.ts +++ b/src/sandboxes/__tests__/runClaudeCodeAgent.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; vi.mock("@trigger.dev/sdk/v3", () => ({ logger: { log: vi.fn(), error: vi.fn() }, @@ -12,6 +12,8 @@ vi.mock("../logStep", () => ({ const { runClaudeCodeAgent } = await import("../runClaudeCodeAgent"); const { logStep } = await import("../logStep"); +const originalEnv = { ...process.env }; + function mockDetachedCommand(finished: { exitCode: number; stdout: () => Promise; @@ -26,6 +28,11 @@ function createMockSandbox() { beforeEach(() => { vi.clearAllMocks(); + delete process.env.CLAUDE_CODE_OAUTH_TOKEN; +}); + +afterEach(() => { + process.env = { ...originalEnv }; }); describe("runClaudeCodeAgent", () => { @@ -147,4 +154,76 @@ describe("runClaudeCodeAgent", () => { stderr: "fatal error\n", }); }); + + it("injects CLAUDE_CODE_OAUTH_TOKEN from process.env when no env is provided", async () => { + process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth-test-token"; + const sandbox = createMockSandbox(); + sandbox.runCommand.mockResolvedValueOnce( + mockDetachedCommand({ + exitCode: 0, + stdout: async () => "", + stderr: async () => "", + }), + ); + + await runClaudeCodeAgent(sandbox, { + label: "Test", + message: "Do something", + }); + + expect(sandbox.runCommand).toHaveBeenCalledWith( + expect.objectContaining({ + env: { CLAUDE_CODE_OAUTH_TOKEN: "oauth-test-token" }, + }), + ); + }); + + it("merges CLAUDE_CODE_OAUTH_TOKEN into provided env", async () => { + process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth-test-token"; + const sandbox = createMockSandbox(); + sandbox.runCommand.mockResolvedValueOnce( + mockDetachedCommand({ + exitCode: 0, + stdout: async () => "", + stderr: async () => "", + }), + ); + + await runClaudeCodeAgent(sandbox, { + label: "Test", + message: "Do something", + env: { GITHUB_TOKEN: "ghp_test" }, + }); + + expect(sandbox.runCommand).toHaveBeenCalledWith( + expect.objectContaining({ + env: { + GITHUB_TOKEN: "ghp_test", + CLAUDE_CODE_OAUTH_TOKEN: "oauth-test-token", + }, + }), + ); + }); + + it("does not set env when no CLAUDE_CODE_OAUTH_TOKEN and no env provided", async () => { + const sandbox = createMockSandbox(); + sandbox.runCommand.mockResolvedValueOnce( + mockDetachedCommand({ + exitCode: 0, + stdout: async () => "", + stderr: async () => "", + }), + ); + + await runClaudeCodeAgent(sandbox, { + label: "Test", + message: "Do something", + }); + + expect(sandbox.runCommand).toHaveBeenCalledWith({ + cmd: "claude", + args: ["-p", "--dangerously-skip-permissions", "Do something"], + detached: true, + }); + }); }); diff --git a/src/sandboxes/getSandboxEnv.ts b/src/sandboxes/getSandboxEnv.ts index e847e64..3906254 100644 --- a/src/sandboxes/getSandboxEnv.ts +++ b/src/sandboxes/getSandboxEnv.ts @@ -20,10 +20,5 @@ export function getSandboxEnv( env.GITHUB_TOKEN = githubToken; } - const claudeCodeOauthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN; - if (claudeCodeOauthToken) { - env.CLAUDE_CODE_OAUTH_TOKEN = claudeCodeOauthToken; - } - return env; } diff --git a/src/sandboxes/runClaudeCodeAgent.ts b/src/sandboxes/runClaudeCodeAgent.ts index 2026e5a..2d15397 100644 --- a/src/sandboxes/runClaudeCodeAgent.ts +++ b/src/sandboxes/runClaudeCodeAgent.ts @@ -31,14 +31,20 @@ export async function runClaudeCodeAgent( logStep(label, true, { cmd: "claude", args }); + const claudeOauthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN; + const mergedEnv: Record = { + ...(env ?? {}), + ...(claudeOauthToken ? { CLAUDE_CODE_OAUTH_TOKEN: claudeOauthToken } : {}), + }; + const commandOpts: Record = { cmd: "claude", args, detached: true, }; - if (env) { - commandOpts.env = env; + if (Object.keys(mergedEnv).length > 0) { + commandOpts.env = mergedEnv; } const command = await sandbox.runCommand(commandOpts as any); From 7bf6aea94e46dfd95dce227b5ee6273f1d142f5f Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 13 Mar 2026 08:03:30 -0500 Subject: [PATCH 3/3] refactor: simplify CLAUDE_CODE_OAUTH_TOKEN injection (KISS) Always spread process.env.CLAUDE_CODE_OAUTH_TOKEN into the command env instead of conditional merge logic. Co-Authored-By: Claude Opus 4.6 --- .../__tests__/runClaudeCodeAgent.test.ts | 84 ++----------------- src/sandboxes/runClaudeCodeAgent.ts | 14 +--- 2 files changed, 12 insertions(+), 86 deletions(-) diff --git a/src/sandboxes/__tests__/runClaudeCodeAgent.test.ts b/src/sandboxes/__tests__/runClaudeCodeAgent.test.ts index e9e43aa..60fe953 100644 --- a/src/sandboxes/__tests__/runClaudeCodeAgent.test.ts +++ b/src/sandboxes/__tests__/runClaudeCodeAgent.test.ts @@ -28,7 +28,7 @@ function createMockSandbox() { beforeEach(() => { vi.clearAllMocks(); - delete process.env.CLAUDE_CODE_OAUTH_TOKEN; + process.env.CLAUDE_CODE_OAUTH_TOKEN = "test-oauth-token"; }); afterEach(() => { @@ -36,7 +36,7 @@ afterEach(() => { }); describe("runClaudeCodeAgent", () => { - it("calls claude --print with the message", async () => { + it("calls claude --print with the message and injects CLAUDE_CODE_OAUTH_TOKEN", async () => { const sandbox = createMockSandbox(); sandbox.runCommand.mockResolvedValueOnce( mockDetachedCommand({ @@ -55,10 +55,11 @@ describe("runClaudeCodeAgent", () => { cmd: "claude", args: ["-p", "--dangerously-skip-permissions", "Fix the bug"], detached: true, + env: { CLAUDE_CODE_OAUTH_TOKEN: "test-oauth-token" }, }); }); - it("passes env vars when provided", async () => { + it("merges CLAUDE_CODE_OAUTH_TOKEN into provided env", async () => { const sandbox = createMockSandbox(); sandbox.runCommand.mockResolvedValueOnce( mockDetachedCommand({ @@ -78,7 +79,10 @@ describe("runClaudeCodeAgent", () => { cmd: "claude", args: ["-p", "--dangerously-skip-permissions", "Update the README"], detached: true, - env: { GITHUB_TOKEN: "ghp_test" }, + env: { + GITHUB_TOKEN: "ghp_test", + CLAUDE_CODE_OAUTH_TOKEN: "test-oauth-token", + }, }); }); @@ -154,76 +158,4 @@ describe("runClaudeCodeAgent", () => { stderr: "fatal error\n", }); }); - - it("injects CLAUDE_CODE_OAUTH_TOKEN from process.env when no env is provided", async () => { - process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth-test-token"; - const sandbox = createMockSandbox(); - sandbox.runCommand.mockResolvedValueOnce( - mockDetachedCommand({ - exitCode: 0, - stdout: async () => "", - stderr: async () => "", - }), - ); - - await runClaudeCodeAgent(sandbox, { - label: "Test", - message: "Do something", - }); - - expect(sandbox.runCommand).toHaveBeenCalledWith( - expect.objectContaining({ - env: { CLAUDE_CODE_OAUTH_TOKEN: "oauth-test-token" }, - }), - ); - }); - - it("merges CLAUDE_CODE_OAUTH_TOKEN into provided env", async () => { - process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth-test-token"; - const sandbox = createMockSandbox(); - sandbox.runCommand.mockResolvedValueOnce( - mockDetachedCommand({ - exitCode: 0, - stdout: async () => "", - stderr: async () => "", - }), - ); - - await runClaudeCodeAgent(sandbox, { - label: "Test", - message: "Do something", - env: { GITHUB_TOKEN: "ghp_test" }, - }); - - expect(sandbox.runCommand).toHaveBeenCalledWith( - expect.objectContaining({ - env: { - GITHUB_TOKEN: "ghp_test", - CLAUDE_CODE_OAUTH_TOKEN: "oauth-test-token", - }, - }), - ); - }); - - it("does not set env when no CLAUDE_CODE_OAUTH_TOKEN and no env provided", async () => { - const sandbox = createMockSandbox(); - sandbox.runCommand.mockResolvedValueOnce( - mockDetachedCommand({ - exitCode: 0, - stdout: async () => "", - stderr: async () => "", - }), - ); - - await runClaudeCodeAgent(sandbox, { - label: "Test", - message: "Do something", - }); - - expect(sandbox.runCommand).toHaveBeenCalledWith({ - cmd: "claude", - args: ["-p", "--dangerously-skip-permissions", "Do something"], - detached: true, - }); - }); }); diff --git a/src/sandboxes/runClaudeCodeAgent.ts b/src/sandboxes/runClaudeCodeAgent.ts index 2d15397..68896f9 100644 --- a/src/sandboxes/runClaudeCodeAgent.ts +++ b/src/sandboxes/runClaudeCodeAgent.ts @@ -31,22 +31,16 @@ export async function runClaudeCodeAgent( logStep(label, true, { cmd: "claude", args }); - const claudeOauthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN; - const mergedEnv: Record = { - ...(env ?? {}), - ...(claudeOauthToken ? { CLAUDE_CODE_OAUTH_TOKEN: claudeOauthToken } : {}), - }; - const commandOpts: Record = { cmd: "claude", args, detached: true, + env: { + ...env, + CLAUDE_CODE_OAUTH_TOKEN: process.env.CLAUDE_CODE_OAUTH_TOKEN, + }, }; - if (Object.keys(mergedEnv).length > 0) { - commandOpts.env = mergedEnv; - } - const command = await sandbox.runCommand(commandOpts as any); const result = await command.wait();