From 09cc80bfc49c9458f2b94429a5dc1e44b3549d06 Mon Sep 17 00:00:00 2001 From: Ben Schellenberger <2601492+bschellenberger2600@users.noreply.github.com> Date: Mon, 15 Jun 2026 23:28:58 -0400 Subject: [PATCH] Disable interactive git credential prompts on network operations. Set GIT_TERMINAL_PROMPT=0 on fetch and push subprocesses so batch CLIs never leak Username/password prompts to the TTY when credentials are missing. Co-authored-by: Cursor --- git/command.go | 31 +++++++++++++++++++++++ git/command_test.go | 61 +++++++++++++++++++++++++++++++++++++++++++++ git/operations.go | 4 +++ 3 files changed, 96 insertions(+) create mode 100644 git/command.go create mode 100644 git/command_test.go diff --git a/git/command.go b/git/command.go new file mode 100644 index 0000000..f218b84 --- /dev/null +++ b/git/command.go @@ -0,0 +1,31 @@ +package git + +import ( + "os" + "os/exec" + "strings" +) + +const gitTerminalPromptKey = "GIT_TERMINAL_PROMPT=" + +// nonInteractiveGitEnv returns a copy of env with GIT_TERMINAL_PROMPT=0 so git +// never prompts for credentials on the controlling TTY during batch operations. +func nonInteractiveGitEnv(env []string) []string { + if env == nil { + env = os.Environ() + } + out := make([]string, 0, len(env)+1) + for _, e := range env { + if strings.HasPrefix(e, gitTerminalPromptKey) { + continue + } + out = append(out, e) + } + return append(out, "GIT_TERMINAL_PROMPT=0") +} + +// PrepareNetworkGit configures cmd for fetch/push operations that may contact +// a remote. Callers must still set cmd.Dir (and stdout/stderr as needed). +func PrepareNetworkGit(cmd *exec.Cmd) { + cmd.Env = nonInteractiveGitEnv(cmd.Env) +} diff --git a/git/command_test.go b/git/command_test.go new file mode 100644 index 0000000..eb87df0 --- /dev/null +++ b/git/command_test.go @@ -0,0 +1,61 @@ +package git + +import ( + "os/exec" + "strings" + "testing" + + testutil "github.com/git-fire/git-testkit" +) + +func TestNonInteractiveGitEnv_OverridesExistingPrompt(t *testing.T) { + env := nonInteractiveGitEnv([]string{ + "HOME=/tmp", + "GIT_TERMINAL_PROMPT=1", + "PATH=/bin", + }) + if !containsEnv(env, "GIT_TERMINAL_PROMPT=0") { + t.Fatalf("expected GIT_TERMINAL_PROMPT=0 in env, got %#v", env) + } + for _, e := range env { + if e == "GIT_TERMINAL_PROMPT=1" { + t.Fatalf("did not override existing GIT_TERMINAL_PROMPT: %#v", env) + } + } +} + +func TestPrepareNetworkGit_SetsEnvOnCommand(t *testing.T) { + cmd := exec.Command("git", "version") + PrepareNetworkGit(cmd) + if !containsEnv(cmd.Env, "GIT_TERMINAL_PROMPT=0") { + t.Fatalf("prepareNetworkGit did not set GIT_TERMINAL_PROMPT=0: %#v", cmd.Env) + } +} + +func TestFetchRemote_UnauthenticatedHTTPSFailsWithoutPrompt(t *testing.T) { + repo := testutil.CreateTestRepo(t, testutil.RepoOptions{ + Name: "https-fetch-repo", + Remotes: map[string]string{ + "origin": "https://github.com/git-fire/nonexistent-repo-auth-test.git", + }, + }) + + err := FetchRemote(repo, "origin") + if err == nil { + t.Fatal("expected fetch to fail without credentials") + } + msg := strings.ToLower(err.Error()) + if !strings.Contains(msg, "terminal prompts disabled") && + !strings.Contains(msg, "could not read username") { + t.Fatalf("expected non-interactive auth failure, got: %v", err) + } +} + +func containsEnv(env []string, want string) bool { + for _, e := range env { + if e == want { + return true + } + } + return false +} diff --git a/git/operations.go b/git/operations.go index ff85242..6c91210 100644 --- a/git/operations.go +++ b/git/operations.go @@ -87,6 +87,7 @@ func IsDirty(repoPath string) (bool, error) { func DetectConflict(repoPath, branch, remote string) (bool, string, string, error) { cmd := exec.Command("git", "fetch", remote) cmd.Dir = repoPath + PrepareNetworkGit(cmd) if output, err := cmd.CombinedOutput(); err != nil { return false, "", "", commandError("git fetch", err, output) } @@ -162,6 +163,7 @@ func RefIsAncestor(repoPath, ancestorRef, descendantRef string) (bool, error) { func FetchRemote(repoPath, remote string) error { cmd := exec.Command("git", "fetch", remote) cmd.Dir = repoPath + PrepareNetworkGit(cmd) if output, err := cmd.CombinedOutput(); err != nil { return commandError("git fetch", err, output) } @@ -191,6 +193,7 @@ func CreateFireBranch(repoPath, originalBranch, localSHA string) (string, error) func PushBranch(repoPath, remote, branch string) error { cmd := exec.Command("git", "push", remote, branch) cmd.Dir = repoPath + PrepareNetworkGit(cmd) var stderr bytes.Buffer cmd.Stderr = &stderr if err := cmd.Run(); err != nil { @@ -203,6 +206,7 @@ func PushBranch(repoPath, remote, branch string) error { func PushAllBranches(repoPath, remote string) error { cmd := exec.Command("git", "push", remote, "--all") cmd.Dir = repoPath + PrepareNetworkGit(cmd) var stderr bytes.Buffer cmd.Stderr = &stderr if err := cmd.Run(); err != nil {