Skip to content

fix: strip betterleaks env vars so git subprocesses can authenticate#769

Open
dipree wants to merge 1 commit intomainfrom
fix/betterleaks-env-poisoning-breaks-git-auth
Open

fix: strip betterleaks env vars so git subprocesses can authenticate#769
dipree wants to merge 1 commit intomainfrom
fix/betterleaks-env-poisoning-breaks-git-auth

Conversation

@dipree
Copy link
Contributor

@dipree dipree commented Mar 25, 2026

Summary

The betterleaks library (used for secret redaction) has an init() in config/config.go that unconditionally sets process-wide environment variables:

func init() {
    env := map[string]string{
        "GIT_CONFIG_GLOBAL":      "/dev/null",
        "GIT_TERMINAL_PROMPT":    "0",
        "GIT_NO_REPLACE_OBJECTS": "1",
        "GIT_CONFIG_NOSYSTEM":    "1",
    }
    for key, value := range env {
        os.Setenv(key, value)
    }
}

Since Go's init() runs at import time, this poisons the entire process environment. When the pre-push hook spawns git push/git fetch subprocesses (in push_common.go), they inherit GIT_CONFIG_GLOBAL=/dev/null and GIT_CONFIG_NOSYSTEM=1, which prevents git from reading ~/.gitconfig — where credential helpers like gh auth git-credential are configured. The result is every pre-push hook sync failing with:

fatal: could not read Username for 'https://github.com': terminal prompts disabled

Fix

Added gitSubprocessEnv() helper that strips the betterleaks-injected vars (GIT_CONFIG_GLOBAL, GIT_CONFIG_NOSYSTEM, GIT_NO_REPLACE_OBJECTS) from the environment before spawning git subprocesses, while keeping GIT_TERMINAL_PROMPT=0 to prevent interactive prompts in hook contexts.

Applied to both tryPushSessionsCommon (push) and fetchAndMergeSessionsCommon (fetch).

Note

checkpoint_remote.go and trail_cmd.go have the same latent bug — they use os.Environ() directly which includes the poisoned vars. They may work today because their operations either don't require auth or fail silently, but should be updated to use gitSubprocessEnv() too.

Longer term, the betterleaks library should scope these env vars to its own subprocesses rather than setting them process-wide.

Test plan

  • Built the fixed binary and ran entire hooks git pre-push origin — all syncs succeed
  • Run existing tests: go test ./cmd/entire/cli/strategy/...

🤖 Generated with Claude Code


Note

Low Risk
Low risk: change is isolated to pre-push git subprocess environment setup and primarily affects authentication behavior; main failure mode would be altered credential/config resolution for these commands.

Overview
Fixes hook-driven session sync failures by introducing gitSubprocessEnv() to remove betterleaks-injected vars (GIT_CONFIG_GLOBAL, GIT_CONFIG_NOSYSTEM, GIT_NO_REPLACE_OBJECTS) from the environment passed to git subprocesses, while explicitly enforcing GIT_TERMINAL_PROMPT=0.

Applies this sanitized environment to the git push in tryPushSessionsCommon and the git fetch in fetchAndMergeSessionsCommon, restoring access to user credential helpers/config during non-interactive hook runs.

Written by Cursor Bugbot for commit 5e86ac4. Configure here.

The betterleaks library sets GIT_CONFIG_GLOBAL=/dev/null and
GIT_CONFIG_NOSYSTEM=1 in an init() function, which poisons the
entire process environment. When the pre-push hook spawns git
push/fetch subprocesses, they inherit these vars and cannot read
~/.gitconfig where credential helpers (e.g. gh auth git-credential)
are configured — causing "could not read Username" auth failures.

Add gitSubprocessEnv() that strips these vars before spawning git
subprocesses, restoring credential helper access.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: cd16ae92afce
@dipree dipree requested a review from a team as a code owner March 25, 2026 12:24
Copilot AI review requested due to automatic review settings March 25, 2026 12:24
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes git authentication failures in hook-spawned git push/git fetch by ensuring subprocesses don’t inherit the betterleaks-poisoned git environment variables, restoring access to user credential helpers configured via ~/.gitconfig.

Changes:

  • Added a gitSubprocessEnv() helper to filter out betterleaks-injected git env vars before spawning git subprocesses.
  • Applied the filtered environment to the git push and git fetch subprocesses used by pre-push sync logic.

Comment on lines +27 to +37
// strips those vars so that git can authenticate normally, while keeping
// GIT_TERMINAL_PROMPT=0 to prevent interactive prompts in hook/non-TTY contexts.
func gitSubprocessEnv() []string {
env := os.Environ()
filtered := make([]string, 0, len(env))
for _, e := range env {
if strings.HasPrefix(e, "GIT_CONFIG_GLOBAL=") ||
strings.HasPrefix(e, "GIT_CONFIG_NOSYSTEM=") ||
strings.HasPrefix(e, "GIT_NO_REPLACE_OBJECTS=") {
continue
}
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gitSubprocessEnv() currently strips GIT_CONFIG_GLOBAL / GIT_CONFIG_NOSYSTEM / GIT_NO_REPLACE_OBJECTS unconditionally (based only on key prefix). That also removes any user-supplied values (e.g., a custom GIT_CONFIG_GLOBAL path), which is broader than “strip betterleaks-injected vars” and could change behavior unexpectedly. Consider only stripping when the value matches the known betterleaks poison values (e.g., GIT_CONFIG_GLOBAL=/dev/null, GIT_CONFIG_NOSYSTEM=1, GIT_NO_REPLACE_OBJECTS=1), or otherwise scoping the removal to cases you can attribute to betterleaks.

Suggested change
// strips those vars so that git can authenticate normally, while keeping
// GIT_TERMINAL_PROMPT=0 to prevent interactive prompts in hook/non-TTY contexts.
func gitSubprocessEnv() []string {
env := os.Environ()
filtered := make([]string, 0, len(env))
for _, e := range env {
if strings.HasPrefix(e, "GIT_CONFIG_GLOBAL=") ||
strings.HasPrefix(e, "GIT_CONFIG_NOSYSTEM=") ||
strings.HasPrefix(e, "GIT_NO_REPLACE_OBJECTS=") {
continue
}
// strips those known poison values so that git can authenticate normally, while keeping
// GIT_TERMINAL_PROMPT=0 to prevent interactive prompts in hook/non-TTY contexts. Any
// user-specified values for these variables are preserved.
func gitSubprocessEnv() []string {
env := os.Environ()
filtered := make([]string, 0, len(env))
for _, e := range env {
parts := strings.SplitN(e, "=", 2)
if len(parts) != 2 {
// Malformed entry; preserve as-is.
filtered = append(filtered, e)
continue
}
key := parts[0]
value := parts[1]
switch key {
case "GIT_CONFIG_GLOBAL":
// Strip only the betterleaks poison value.
if value == "/dev/null" {
continue
}
case "GIT_CONFIG_NOSYSTEM":
// Strip only the betterleaks poison value.
if value == "1" {
continue
}
case "GIT_NO_REPLACE_OBJECTS":
// Strip only the betterleaks poison value.
if value == "1" {
continue
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +40
env := os.Environ()
filtered := make([]string, 0, len(env))
for _, e := range env {
if strings.HasPrefix(e, "GIT_CONFIG_GLOBAL=") ||
strings.HasPrefix(e, "GIT_CONFIG_NOSYSTEM=") ||
strings.HasPrefix(e, "GIT_NO_REPLACE_OBJECTS=") {
continue
}
filtered = append(filtered, e)
}
return append(filtered, "GIT_TERMINAL_PROMPT=0")
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gitSubprocessEnv() appends "GIT_TERMINAL_PROMPT=0" but does not remove an existing GIT_TERMINAL_PROMPT entry from os.Environ(). On systems where the first occurrence wins (common for getenv), this won’t reliably force prompts off if the variable is already set to a different value, and it can create duplicate keys. Filter out any existing GIT_TERMINAL_PROMPT=... entry before appending (or set/replace it in-place) to ensure the subprocess environment is deterministic.

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +41
// gitSubprocessEnv returns a copy of the current process environment suitable for
// spawning git subprocesses that need credential-helper access.
//
// The betterleaks library (used for secret redaction) sets GIT_CONFIG_GLOBAL=/dev/null
// and GIT_CONFIG_NOSYSTEM=1 in its init(), which prevents git from reading ~/.gitconfig
// where credential helpers (e.g. gh auth git-credential) are configured. This function
// strips those vars so that git can authenticate normally, while keeping
// GIT_TERMINAL_PROMPT=0 to prevent interactive prompts in hook/non-TTY contexts.
func gitSubprocessEnv() []string {
env := os.Environ()
filtered := make([]string, 0, len(env))
for _, e := range env {
if strings.HasPrefix(e, "GIT_CONFIG_GLOBAL=") ||
strings.HasPrefix(e, "GIT_CONFIG_NOSYSTEM=") ||
strings.HasPrefix(e, "GIT_NO_REPLACE_OBJECTS=") {
continue
}
filtered = append(filtered, e)
}
return append(filtered, "GIT_TERMINAL_PROMPT=0")
}
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new env-filtering behavior is central to fixing auth failures, but there’s no unit test exercising it. Since this package already has push_common_test.go, add a focused test for gitSubprocessEnv() that sets process env vars (including the betterleaks ones and an existing GIT_TERMINAL_PROMPT value) and asserts the returned slice removes the poisoned vars and reliably sets GIT_TERMINAL_PROMPT=0 exactly once.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants