Skip to content

Commit e3be905

Browse files
wesmclaude
andcommitted
fix: normalize relative path env vars and isolate copilot config
- Resolve path-valued env vars (GOOGLE_APPLICATION_CREDENTIALS, CURL_CA_BUNDLE) to absolute paths in cleanEnv() so they remain valid when subprocesses run from os.TempDir() - Use --config-dir with a temporary empty directory for copilot to prevent user-configured MCP servers in ~/.copilot/ from loading - Add TestCleanEnv_NormalizesRelativePaths regression test Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7467c1b commit e3be905

2 files changed

Lines changed: 81 additions & 14 deletions

File tree

internal/insight/generate.go

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"io"
99
"os"
1010
"os/exec"
11+
"path/filepath"
1112
"strings"
1213
)
1314

@@ -148,17 +149,35 @@ func envKeyAllowed(key string) bool {
148149
return false
149150
}
150151

152+
// pathValuedKeys lists env var keys whose values are file
153+
// paths. These must be resolved to absolute paths before
154+
// passing to subprocesses that run in a different directory.
155+
var pathValuedKeys = map[string]bool{
156+
"GOOGLE_APPLICATION_CREDENTIALS": true,
157+
"CURL_CA_BUNDLE": true,
158+
}
159+
151160
// cleanEnv returns an allowlisted subset of the current
152161
// environment for agent CLI subprocesses, plus
153-
// CLAUDE_NO_SOUND=1.
162+
// CLAUDE_NO_SOUND=1. Path-valued env vars are resolved to
163+
// absolute paths so they remain valid when the subprocess
164+
// runs from a different working directory.
154165
func cleanEnv() []string {
155166
env := os.Environ()
156167
filtered := make([]string, 0, len(env))
157168
for _, e := range env {
158-
k, _, _ := strings.Cut(e, "=")
159-
if envKeyAllowed(k) {
160-
filtered = append(filtered, e)
169+
k, v, _ := strings.Cut(e, "=")
170+
if !envKeyAllowed(k) {
171+
continue
172+
}
173+
if pathValuedKeys[strings.ToUpper(k)] &&
174+
v != "" && !filepath.IsAbs(v) {
175+
abs, err := filepath.Abs(v)
176+
if err == nil {
177+
e = k + "=" + abs
178+
}
161179
}
180+
filtered = append(filtered, e)
162181
}
163182
return append(filtered, "CLAUDE_NO_SOUND=1")
164183
}
@@ -459,16 +478,27 @@ func parseCodexStream(
459478
// generateCopilot invokes `copilot -p <prompt> --silent`.
460479
// The prompt is passed as the -p argument (copilot does not
461480
// read prompts from stdin). Output is plain text on stdout.
481+
// An empty --config-dir isolates copilot from user-configured
482+
// MCP servers in ~/.copilot/.
462483
func generateCopilot(
463484
ctx context.Context, path, prompt string, onLog LogFunc,
464485
) (Result, error) {
486+
configDir, err := os.MkdirTemp("", "copilot-insight-*")
487+
if err != nil {
488+
return Result{}, fmt.Errorf(
489+
"create copilot config dir: %w", err,
490+
)
491+
}
492+
defer os.RemoveAll(configDir)
493+
465494
cmd := exec.CommandContext(
466495
ctx, path,
467496
"-p", prompt,
468497
"--silent",
469498
"--no-custom-instructions",
470499
"--no-ask-user",
471500
"--disable-builtin-mcps",
501+
"--config-dir", configDir,
472502
)
473503
cmd.Dir = os.TempDir()
474504
cmd.Env = cleanEnv()

internal/insight/generate_test.go

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,44 @@ func TestCleanEnv(t *testing.T) {
228228
}
229229
}
230230

231+
func TestCleanEnv_NormalizesRelativePaths(t *testing.T) {
232+
t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "creds/svc.json")
233+
t.Setenv("CURL_CA_BUNDLE", "certs/ca.pem")
234+
235+
env := cleanEnv()
236+
envMap := make(map[string]string, len(env))
237+
for _, e := range env {
238+
k, v, _ := strings.Cut(e, "=")
239+
envMap[strings.ToUpper(k)] = v
240+
}
241+
242+
cwd, err := os.Getwd()
243+
if err != nil {
244+
t.Fatal(err)
245+
}
246+
247+
for _, key := range []string{
248+
"GOOGLE_APPLICATION_CREDENTIALS",
249+
"CURL_CA_BUNDLE",
250+
} {
251+
v, ok := envMap[key]
252+
if !ok {
253+
t.Fatalf("%s missing from env", key)
254+
}
255+
if !filepath.IsAbs(v) {
256+
t.Errorf(
257+
"%s = %q, want absolute path", key, v,
258+
)
259+
}
260+
if !strings.HasPrefix(v, cwd) {
261+
t.Errorf(
262+
"%s = %q, want prefix %q",
263+
key, v, cwd,
264+
)
265+
}
266+
}
267+
}
268+
231269
func TestEnvKeyAllowed(t *testing.T) {
232270
tests := []struct {
233271
key string
@@ -472,21 +510,20 @@ func TestGenerateCopilot_CLIFlags(t *testing.T) {
472510
strings.TrimSpace(string(argsData)), "\n",
473511
)
474512

475-
wantArgs := []string{
476-
"-p", "test prompt",
513+
// --config-dir value is a dynamic temp path, so verify
514+
// args as a joined string for the fixed flags.
515+
joined := strings.Join(args, " ")
516+
for _, want := range []string{
517+
"-p test prompt",
477518
"--silent",
478519
"--no-custom-instructions",
479520
"--no-ask-user",
480521
"--disable-builtin-mcps",
481-
}
482-
if len(args) != len(wantArgs) {
483-
t.Fatalf("args = %v, want %v", args, wantArgs)
484-
}
485-
for i, want := range wantArgs {
486-
if args[i] != want {
522+
"--config-dir",
523+
} {
524+
if !strings.Contains(joined, want) {
487525
t.Errorf(
488-
"arg[%d] = %q, want %q",
489-
i, args[i], want,
526+
"args %q missing %q", joined, want,
490527
)
491528
}
492529
}

0 commit comments

Comments
 (0)