From 74471f5eadde963ccaf28443b92d5e61ecfa2d31 Mon Sep 17 00:00:00 2001 From: Aaron Wong <6979793+zzwong@users.noreply.github.com> Date: Wed, 23 Sep 2026 16:05:03 -0400 Subject: [PATCH] fix(llmadapters): launch Claude reviewers outside the PR checkout (#623) Claude Code 2.1.281 rejects a working directory with its own .claude/ unless that exact directory is trusted, so every background reviewer failed on repositories that commit .claude/. Launching there also loaded the PR's project settings and hooks. Background reviewers now start from the adapter work directory, as foreground mode already does, and reach the checkout through --add-dir. The prompt file names the checkout path. --- internal/llmadapters/subprocess.go | 21 +++++++++++++-------- internal/llmadapters/subprocess_test.go | 8 ++++++-- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/internal/llmadapters/subprocess.go b/internal/llmadapters/subprocess.go index 33a228b..5c3b9f3 100644 --- a/internal/llmadapters/subprocess.go +++ b/internal/llmadapters/subprocess.go @@ -261,7 +261,7 @@ func (a *SubprocessAdapter) startClaudeBG(ctx context.Context, req Request, resu _ = cleanup() return nil, err } - if err := writeClaudeBGPromptFile(req.Prompt, scratch); err != nil { + if err := writeClaudeBGPromptFile(req.Prompt, scratch, req.ReviewerWorkspace); err != nil { _ = cleanup() return nil, err } @@ -281,16 +281,14 @@ func (a *SubprocessAdapter) startClaudeBG(ctx context.Context, req Request, resu } execArgs := append(append([]string(nil), a.commandArgsPrefix...), args...) - launchDir := workDir - if req.ReviewerWorkspace != nil { - launchDir = req.ReviewerWorkspace.RepoDir - } + // Never launch inside the reviewer checkout: Claude would load the PR's + // .claude/ project settings. The checkout is reachable through --add-dir. env, err := a.processEnv(req, scratch) if err != nil { _ = cleanup() return nil, err } - process, err := launchProcess(ctx, a.command, execArgs, launchDir, env, a.timeout, req.LogPath, cleanup, false) + process, err := launchProcess(ctx, a.command, execArgs, workDir, env, a.timeout, req.LogPath, cleanup, false) if err != nil { return nil, err } @@ -506,7 +504,7 @@ func (a *SubprocessAdapter) startClaudeForeground(ctx context.Context, req Reque _ = cleanup() return nil, err } - if err := writeClaudeBGPromptFile(req.Prompt, scratch); err != nil { + if err := writeClaudeBGPromptFile(req.Prompt, scratch, req.ReviewerWorkspace); err != nil { _ = cleanup() return nil, err } @@ -1974,15 +1972,22 @@ func claudeBGSprintedValue(value any) string { return stringValue } -func writeClaudeBGPromptFile(prompt string, scratch string) error { +func writeClaudeBGPromptFile(prompt string, scratch string, workspace *ReviewerWorkspaceRequest) error { promptPath := filepath.Join(scratch, claudeBGPromptFilename) resultPath := filepath.Join(scratch, claudeBGResultFilename) + if workspace != nil { + prompt = claudeReviewerWorkspaceNote(workspace.RepoDir) + "\n\n" + prompt + } if err := os.WriteFile(promptPath, []byte(wrapClaudeBGPrompt(prompt, resultPath)), 0o600); err != nil { return fmt.Errorf("llm subprocess: writing Claude bg prompt file: %w", err) } return nil } +func claudeReviewerWorkspaceNote(repoDir string) string { + return fmt.Sprintf("The pull request checkout is at %s. Repository-relative paths in these instructions are relative to it. Your shell does not start there: run commands with `cd %q && ...` or `git -C %q`.", repoDir, repoDir, repoDir) +} + func claudeBGPositionalPrompt(scratch string) string { promptPath := filepath.Join(scratch, claudeBGPromptFilename) resultPath := filepath.Join(scratch, claudeBGResultFilename) diff --git a/internal/llmadapters/subprocess_test.go b/internal/llmadapters/subprocess_test.go index fd0d5e1..9f789a8 100644 --- a/internal/llmadapters/subprocess_test.go +++ b/internal/llmadapters/subprocess_test.go @@ -261,8 +261,12 @@ func TestSubprocessClaudeReviewerWorkspaceLaunch(t *testing.T) { if !containsSamePath(addDirs, record.AddDir) || !containsSamePath(addDirs, repoRoot) { t.Fatalf("--add-dir values = %#v, want scratch %q and repo %q", addDirs, record.AddDir, repoRoot) } - if !samePath(t, record.Cwd, repoRoot) { - t.Fatalf("cwd = %q, want reviewer workspace repo %q", record.Cwd, repoRoot) + // Launching inside the PR checkout would load its .claude/ project settings. + if wantWorkDir := filepath.Join(filepath.Dir(recordPath), "claude-bg-workdir"); !samePath(t, record.Cwd, wantWorkDir) { + t.Fatalf("cwd = %q, want Claude bg workdir %q outside the reviewer checkout", record.Cwd, wantWorkDir) + } + if !strings.Contains(record.PromptFile, repoRoot) { + t.Fatalf("prompt file = %q, want reviewer checkout path %q", record.PromptFile, repoRoot) } if tools := flagValue(record.AdapterArgs, "--tools"); tools != "Read,Write,Bash" { t.Fatalf("--tools = %q, want reviewer workspace tools", tools)