From 601dd346b00b357a0541239fb80b34c3795e7c33 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Mon, 11 May 2026 14:32:15 +0100 Subject: [PATCH 1/3] fix(copilot): hint to run copilot directly when exec fails When the copilot binary is found in PATH but exec fails (e.g., due to unusual characters like parentheses in the path on Windows), append a hint suggesting the user run `copilot` directly without `gh`. The hint is only shown when the binary was already present on the host, not when it was freshly downloaded and installed by `gh`. Closes cli/cli#13106 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pkg/cmd/copilot/copilot.go | 21 ++++++++++++++++++--- pkg/cmd/copilot/copilot_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/copilot/copilot.go b/pkg/cmd/copilot/copilot.go index 50b00e9fe..d0cd2e8fc 100644 --- a/pkg/cmd/copilot/copilot.go +++ b/pkg/cmd/copilot/copilot.go @@ -142,8 +142,9 @@ func runCopilot(opts *CopilotOptions) error { return nil } - copilotPath := findCopilotBinary() - if copilotPath == "" { + copilotPath := findCopilotBinaryFunc() + foundInPath := copilotPath != "" + if !foundInPath { if opts.IO.CanPrompt() { confirmed, err := opts.Prompter.Confirm("GitHub Copilot CLI is not installed. Would you like to install it?", true) if err != nil { @@ -175,12 +176,18 @@ func runCopilot(opts *CopilotOptions) error { externalCmd.Stderr = opts.IO.ErrOut externalCmd.Env = append(os.Environ(), "COPILOT_GH=true") - if err := externalCmd.Run(); err != nil { + if err := runExternalCmdFunc(externalCmd); err != nil { if exitErr, ok := err.(*exec.ExitError); ok { // We terminate with os.Exit here, preserving the exit code from Copilot CLI, // and also preventing stdio writes by callers up the stack. os.Exit(exitErr.ExitCode()) } + if foundInPath { + // The binary exists in PATH but exec failed, possibly due to + // unusual characters in the path (see https://github.com/cli/cli/issues/13106). + // Suggest running copilot directly as a workaround. + return fmt.Errorf("%w\nTry running `copilot` directly without `gh`.", err) + } return err } return nil @@ -200,6 +207,14 @@ func copilotBinaryPath() string { return filepath.Join(copilotInstallDir(), binaryName) } +var runExternalCmdFunc = runExternalCmd + +func runExternalCmd(cmd *exec.Cmd) error { + return cmd.Run() +} + +var findCopilotBinaryFunc = findCopilotBinary + // findCopilotBinary returns the path to the Copilot CLI binary, if installed, // with the following order of precedence: // 1. `copilot` in the PATH diff --git a/pkg/cmd/copilot/copilot_test.go b/pkg/cmd/copilot/copilot_test.go index 07e0191e6..16c0b1155 100644 --- a/pkg/cmd/copilot/copilot_test.go +++ b/pkg/cmd/copilot/copilot_test.go @@ -10,6 +10,7 @@ import ( "fmt" "net/http" "os" + "os/exec" "path/filepath" "runtime" "testing" @@ -589,6 +590,32 @@ func TestDownloadCopilot(t *testing.T) { }) } +func TestRunCopilot_execFailureHint(t *testing.T) { + ios, _, _, _ := iostreams.Test() + opts := &CopilotOptions{ + IO: ios, + CopilotArgs: []string{}, + } + + origFind := findCopilotBinaryFunc + findCopilotBinaryFunc = func() string { + return "/usr/bin/copilot" + } + t.Cleanup(func() { findCopilotBinaryFunc = origFind }) + + execErr := fmt.Errorf("exec failed: something went wrong") + origRun := runExternalCmdFunc + runExternalCmdFunc = func(_ *exec.Cmd) error { + return execErr + } + t.Cleanup(func() { runExternalCmdFunc = origRun }) + + err := runCopilot(opts) + require.Error(t, err) + require.ErrorIs(t, err, execErr) + require.Contains(t, err.Error(), "Try running `copilot` directly without `gh`.") +} + func TestCopilotCommandIsSampledAt100(t *testing.T) { spy := &telemetry.CommandRecorderSpy{} factory := &cmdutil.Factory{} From ae7bd54d431cd914b20efab191c6ab357fe24d6a Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Tue, 12 May 2026 12:24:29 +0100 Subject: [PATCH 2/3] fix(copilot): provide full path to copilot binary on exec error Co-authored-by: Kynan Ware <47394200+BagToad@users.noreply.github.com> --- pkg/cmd/copilot/copilot.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/copilot/copilot.go b/pkg/cmd/copilot/copilot.go index d0cd2e8fc..cc83ef48e 100644 --- a/pkg/cmd/copilot/copilot.go +++ b/pkg/cmd/copilot/copilot.go @@ -183,10 +183,10 @@ func runCopilot(opts *CopilotOptions) error { os.Exit(exitErr.ExitCode()) } if foundInPath { - // The binary exists in PATH but exec failed, possibly due to + // We found a `copilot` binary but exec failed, possibly due to // unusual characters in the path (see https://github.com/cli/cli/issues/13106). // Suggest running copilot directly as a workaround. - return fmt.Errorf("%w\nTry running `copilot` directly without `gh`.", err) + return fmt.Errorf("%w\nFailed to run '%s', try running `copilot` directly without `gh`.", err, copilotPath) } return err } From 24c7b25afdb44a12ea823029c3fd7a3092a8be73 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 13:04:19 +0000 Subject: [PATCH 3/3] fix(copilot): update test assertion to match updated error message --- pkg/cmd/copilot/copilot_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/copilot/copilot_test.go b/pkg/cmd/copilot/copilot_test.go index 16c0b1155..fa173f528 100644 --- a/pkg/cmd/copilot/copilot_test.go +++ b/pkg/cmd/copilot/copilot_test.go @@ -613,7 +613,7 @@ func TestRunCopilot_execFailureHint(t *testing.T) { err := runCopilot(opts) require.Error(t, err) require.ErrorIs(t, err, execErr) - require.Contains(t, err.Error(), "Try running `copilot` directly without `gh`.") + require.Contains(t, err.Error(), "try running `copilot` directly without `gh`.") } func TestCopilotCommandIsSampledAt100(t *testing.T) {