Skip to content

[Bug]: git extension: non-dry-run git fetch can block /speckit.specify on a hidden credential prompt #5199

Open

Description

@stefanwalther

Bug Description

The git extension's feature-branch scripts run git fetch --all --prune without GIT_TERMINAL_PROMPT=0 when --dry-run isn't set. The dry-run path already sets it for git ls-remote, but the normal path doesn't:

  • extensions/git/scripts/bash/create-new-feature-branch.sh line 194: git fetch --all --prune >/dev/null 2>&1 || true
  • extensions/git/scripts/powershell/create-new-feature-branch.ps1 line 166: git fetch --all --prune 2>$null | Out-Null
  • extensions/git/scripts/python/create_new_feature_branch.py line 270: subprocess.run(["git", "fetch", "--all", "--prune"], ...) with no env

speckit.git.feature runs as the mandatory before_specify hook, so this fetch runs on every /speckit.specify. If any remote is HTTPS and has no usable credential helper, git asks for a username on the terminal. Git writes that prompt to the tty, so the output redirect doesn't hide it, and the command waits for input. In an agent-driven session, /speckit.specify then appears to hang.

Steps to Reproduce

  1. git init a scratch repo and add an HTTPS remote that needs authentication, for example a private or nonexistent repo: git remote add origin https://github.com/<owner>/<private-repo>.git.
  2. In a terminal, run git -c credential.helper= fetch --all --prune >/dev/null 2>&1.
  3. Run the same command with GIT_TERMINAL_PROMPT=0 in front.

Expected Behavior

The fetch fails fast and the script continues (|| true), as the dry-run path already does for ls-remote.

Actual Behavior

Step 2 prints Username for 'https://github.com': despite the redirect and blocks until input arrives. Step 3 exits 128 immediately with no prompt.

Specify CLI Version

Seen on 1.2.1. The same lines are in v1.2.5 and on main.

AI Agent

Not agent-specific (git extension scripts).

Operating System

macOS

Python Version

3.15

Error Logs

Username for 'https://github.com':

Additional Context

Suggested fix: set GIT_TERMINAL_PROMPT=0 on all three fetch calls (GIT_TERMINAL_PROMPT=0 git fetch ... in bash, $env:GIT_TERMINAL_PROMPT = '0' scoped around the call in PowerShell, env={**os.environ, "GIT_TERMINAL_PROMPT": "0"} in Python). SSH remotes are not covered by this variable; GIT_SSH_COMMAND="ssh -o BatchMode=yes" would be the equivalent if passphrase prompts should also be suppressed.

AI Disclosure

Drafted with Claude Code (model claude-opus-5-5), human-supervised. An AI code review first flagged the missing variable; I reproduced the prompt locally with the steps above before filing.

Activity

  1. github-actions commented on Oct 30, 2026

    @github-actions
    Contributor

    **Bug assessment — git-fetch-prompt-block: Valid · severity medium


    Bug Assessment: git extension fetch can block on hidden credential prompt

    Report (summarized)

    Issue #5199 reports that /speckit.specify can appear to hang when the git extension executes a non-dry-run git fetch --all --prune against an HTTPS remote that requires credentials but has no usable helper. The report includes a minimal reproduction showing that redirecting stdout/stderr does not suppress terminal credential prompts, while GIT_TERMINAL_PROMPT=0 makes the command fail fast.

    Repository evidence matches the report: dry-run remote probing uses GIT_TERMINAL_PROMPT=0 for ls-remote, while non-dry-run fetch paths do not set this variable in Bash, PowerShell, or Python.

    Symptom

    When speckit.git.feature runs before speckit.specify, the non-dry-run fetch can block on an interactive username/password prompt from git, causing /speckit.specify to stall in agent/non-interactive sessions. Expected behavior is fast failure (exit code) with continued script flow, not an interactive wait.

    Reproduction

    1. Initialize a scratch repository and configure an HTTPS remote that requires authentication (or is otherwise credential-challenging).
    2. Run git -c credential.helper= fetch --all --prune >/dev/null 2>&1.
    3. Observe the terminal prompt (for example Username for 'https://github.com':) and blocking behavior.
    4. Run GIT_TERMINAL_PROMPT=0 git -c credential.helper= fetch --all --prune >/dev/null 2>&1 and observe immediate non-interactive failure.
    5. Trigger /speckit.specify with the git extension enabled so the before_specify hook executes speckit.git.feature.

    Suspected Code Paths

    • extensions/git/scripts/bash/create-new-feature-branch.sh:172 — dry-run remote check correctly uses GIT_TERMINAL_PROMPT=0 for git ls-remote.
    • extensions/git/scripts/bash/create-new-feature-branch.sh:194 — non-dry-run branch-number path calls git fetch --all --prune without GIT_TERMINAL_PROMPT=0.
    • extensions/git/scripts/bash/create-new-feature-branch.sh:527 — non-dry-run main flow calls check_existing_branches(..., false, ...), taking the interactive fetch path.
    • extensions/git/scripts/powershell/create-new-feature-branch.ps1:135 — dry-run remote check sets $env:GIT_TERMINAL_PROMPT='0' for git ls-remote.
    • extensions/git/scripts/powershell/create-new-feature-branch.ps1:166 — non-dry-run path runs git fetch --all --prune without non-interactive guard.
    • extensions/git/scripts/powershell/create-new-feature-branch.ps1:483 — non-dry-run main flow calls Get-NextBranchNumber without -SkipFetch.
    • extensions/git/scripts/python/create_new_feature_branch.py:252 — dry-run remote check uses env_extra={"GIT_TERMINAL_PROMPT": "0"} for ls-remote.
    • extensions/git/scripts/python/create_new_feature_branch.py:269-270 — non-dry-run path runs subprocess.run(["git", "fetch", "--all", "--prune"], ...) without env override.
    • extensions/git/scripts/python/create_new_feature_branch.py:526 — non-dry-run main flow calls check_existing_branches(...), entering the unguarded fetch path.
    • extensions/git/extension.yml:47-49 — before_specify hook for speckit.git.feature is optional: false, so this path is routinely exercised when the extension is enabled.

    Root Cause Hypothesis

    The script family intentionally hardens dry-run remote inspection (ls-remote) against interactive prompts, but the corresponding non-dry-run fetch path was not given the same non-interactive environment. Because git credential prompts are tty-directed and not reliably suppressed by redirecting stdout/stderr, these fetch calls can block execution despite output redirection. Confidence: high.

    Proposed Remediation

    Preferred: apply a consistent non-interactive guard to all non-dry-run fetch calls used for branch-number discovery. In Bash, prefix fetch with GIT_TERMINAL_PROMPT=0; in PowerShell, scope $env:GIT_TERMINAL_PROMPT='0' around the fetch and restore prior value; in Python, pass env={**os.environ, "GIT_TERMINAL_PROMPT": "0"} (or existing helper equivalent) to subprocess.run. Keep existing behavior of tolerating fetch failure (|| true / equivalent) so offline and restricted environments continue gracefully.

    Alternatives (optional):

    • Skip fetch entirely and rely only on local refs + specs in non-dry-run mode. Trade-off: avoids prompt risk but can allocate branch numbers behind remote state more often.
    • Also add SSH non-interactive guard (e.g., batch mode) where appropriate. Trade-off: broader behavior change and higher compatibility risk across user SSH setups.

    Files likely to change:

    • extensions/git/scripts/bash/create-new-feature-branch.sh
    • extensions/git/scripts/powershell/create-new-feature-branch.ps1
    • extensions/git/scripts/python/create_new_feature_branch.py
    • tests/extensions/git/test_git_extension_python_parity.py

    Tests to add or update:

    • Add regression tests that assert non-dry-run branch-number discovery invokes fetch with non-interactive environment in all three implementations.
    • Add parity coverage ensuring dry-run and non-dry-run prompt-handling strategy is consistent across Bash/PowerShell/Python.
    • Add a behavior test that simulates credential-challenging remotes and verifies command returns promptly instead of hanging (mock/stub subprocess or git wrapper in deterministic test harness).

    Risks & Considerations

    • PowerShell env scoping must avoid leaking GIT_TERMINAL_PROMPT to later commands.
    • Enforcing non-interactive mode may change user-visible errors from prompt-driven retries to immediate failures; this is desirable for automation but should be documented in test expectations.
    • Ensure no regression in offline/no-git graceful-degradation paths.

    Open Questions

    • [NEEDS CLARIFICATION: Should SSH auth/passphrase prompts also be suppressed in this path for full non-interactive parity, or is HTTPS credential prompt suppression the intended scope?]

    Generated by 🐛 Assess Bug from Labeled Issue for #5199 · copilot · gpt53codex · 22.1 AIC · ⌖ 0.604 AIC · ⊞ 22.4K · ◷

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions