fix: strip betterleaks env vars so git subprocesses can authenticate#769
fix: strip betterleaks env vars so git subprocesses can authenticate#769
Conversation
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
There was a problem hiding this comment.
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 pushandgit fetchsubprocesses used by pre-push sync logic.
| // 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 | ||
| } |
There was a problem hiding this comment.
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.
| // 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 | |
| } | |
| } |
| 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") |
There was a problem hiding this comment.
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.
| // 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") | ||
| } |
There was a problem hiding this comment.
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.
Summary
The
betterleakslibrary (used for secret redaction) has aninit()inconfig/config.gothat unconditionally sets process-wide environment variables:Since Go's
init()runs at import time, this poisons the entire process environment. When the pre-push hook spawnsgit push/git fetchsubprocesses (inpush_common.go), they inheritGIT_CONFIG_GLOBAL=/dev/nullandGIT_CONFIG_NOSYSTEM=1, which prevents git from reading~/.gitconfig— where credential helpers likegh auth git-credentialare configured. The result is every pre-push hook sync failing with: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 keepingGIT_TERMINAL_PROMPT=0to prevent interactive prompts in hook contexts.Applied to both
tryPushSessionsCommon(push) andfetchAndMergeSessionsCommon(fetch).Note
checkpoint_remote.goandtrail_cmd.gohave the same latent bug — they useos.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 usegitSubprocessEnv()too.Longer term, the
betterleakslibrary should scope these env vars to its own subprocesses rather than setting them process-wide.Test plan
entire hooks git pre-push origin— all syncs succeedgo 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 enforcingGIT_TERMINAL_PROMPT=0.Applies this sanitized environment to the
git pushintryPushSessionsCommonand thegit fetchinfetchAndMergeSessionsCommon, restoring access to user credential helpers/config during non-interactive hook runs.Written by Cursor Bugbot for commit 5e86ac4. Configure here.