Skip to content

fix(git): run WSL git reads without a shell - #15257

Merged
OrcaWin merged 5 commits into
mainfrom
OrcaWin/wsl-direct-git-for-reads
Aug 18, 2026
Merged

OrcaWin merged 5 commits into
mainfrom
OrcaWin/wsl-direct-git-for-reads

Conversation

@OrcaWin

@OrcaWin OrcaWin commented Aug 18, 2026 •

Copy link
Copy Markdown
Collaborator
Files Added Deleted Net
Test 4 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​206 $\color{#cf222e}{\Huge{\mathbf{−}}}$​3 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​203
Prod 2 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​117 $\color{#cf222e}{\Huge{\mathbf{−}}}$​7 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​110

Stack

Merge bottom-up; each PR is based on the one above it.

# PR Base
1 #15039 main fix(wsl): pass guest argv verbatim through --exec
2 #15040 #15039 fix(wsl): read machine output from a fenced login shell
3 #15060 #15040 fix(git): fence buffered WSL login-shell reads
4 ▶ #15257 #15060 fix(git): run WSL git reads without a shell ← you are here

ELI5

To run git inside WSL, Orca was starting the user's login shell first — the same one that greets you when you open a terminal — purely so git would be on the PATH. That shell also prints the distro's welcome banner, into the same pipe Orca reads git's answer from. So Orca would read the banner as if it were git's output, and conclude the repo had no GitHub remote.

The fix isn't to filter the banner out. It's to stop starting a shell for work that never needed one: ask the shell once what the PATH is, remember it, then run git directly.

Why the earlier PRs didn't fix #10917

#15040 and #15060 fenced the output so the banner could be sliced off. That treats the symptom. This PR removes the cause for reads.

The shell-free route already existed — --exec /usr/bin/env PATH=… HOME=… /usr/bin/git, added for status reads in #13207 — but it was opt-in, and exactly one options-builder opted in:

$ grep -rn "preferWslDirectGit" src --include=*.ts | grep -v test
git-runtime-options.ts:24:  preferWslDirectGit: true
git-runtime-options.ts:26:  return { ...gitOptionsForWorktree(cwd, options), preferWslDirectGit: true }
status.ts:328:        preferWslDirectGit: true,

Everything else — remote get-url, config --get, log, show, rev-parse — used gitOptionsForWorktree, which never set it. So the exact parse #10917 reports (remote.ts) took the login shell on every call. Fencing it would have been a fifth patch on the same wound.

What changed

Reads are classified at the resolver, not at each caller, so no call site has to remember to opt in:

(options.preferWslDirectGit || isWslDirectGitReadCommand(args)) && …

Writes and network operations deliberately stay on the login shell — they can depend on credential helpers and ssh-agent that only the user's profile sets up.

Subcommands that both read and write require an explicit read flag, so config --get goes direct while config user.email x does not. Same for remote get-url vs remote add, and branch --list vs branch -D.

git show blob reads stop forcing the login shell. The fence added in #15060 stays on that path: the login shell is still the fallback when the environment probe is cold or rejected, and a banner there would become file content. It becomes rarely-hit rather than unnecessary.

Safety

The existing fallback is what makes this low-risk: a direct-git failure retries on the login shell, and a successful retry disables the direct route for that distro. A misclassified command self-heals rather than breaking.

The probe itself already refuses to engage when the environment is unusual — git missing or not executable (exit 127), or XDG_CONFIG_HOME / LD_LIBRARY_PATH / any GIT_* set (exit 78).

Testing

  • 32 classifier cases, weighted toward the dangerous direction: every write form (commit, push, fetch, pull, clone, checkout, add, worktree add, submodule update, config <set>, config --unset, remote add, remote remove, branch -D, branch <name>) is asserted to stay on the login shell.
  • An integration test asserts the [Bug]: git output is parsed from an interactive login shell (-ilc), so any shell-rc banner (fastfetch/neofetch/motd) breaks GitHub source detection #10917 path (remote get-url, config --get) resolves to --exec /usr/bin/env PATH=… with no sh, no -lc, and no getent passwd anywhere in the argv — mutation-checked: reverting the routing fails all four read cases while the three write cases still pass.
  • Suite compared against origin/main at test-name granularity across src/main/git, src/main/ipc, src/shared: zero new failures, 12 fewer than main.
  • Typecheck plus all three static-analysis gates clean.

Behavior note

The first WSL read per distro now also warms the environment probe in the background, so a cold read spawns one extra wsl.exe (once per distro per session). Subsequent reads start no shell at all. Two tests asserted the old spawn sequence and were updated to ignore the probe rather than weakened.

Measured on a real Ubuntu-24.04: shell startup is not the dominant cost (sh -c 67ms vs bash -ilc 70ms; wsl.exe spawn dominates), so this is a correctness and robustness change, not a performance one.

OrcaWin and others added 4 commits August 18, 2026 02:40
WSL-routed git ran through the distro user's interactive login shell for
one reason: to inherit their PATH. That shell also runs the distro's
rc/motd and writes it to the stdout callers parse, which is why #10917
reports a shell banner breaking GitHub source detection.

A shell-free route already existed (`--exec /usr/bin/env PATH=... git`,
added for status reads in #13207) but it was opt-in, and only
gitStatusReadOptionsForWorktree opted in. Every other read -- remote
get-url, config --get, log, show, rev-parse -- took the login shell on
every call, so the reported parse never benefited.

Classify reads at the resolver instead of at each caller. A read needs
nothing the login shell provides, so it takes the direct route without
the caller asking. Writes and network operations stay on the login
shell: they can depend on credential helpers and ssh-agent that the
user's profile sets up.

Subcommands that both read and write (config, remote, branch,
submodule) require an explicit read flag before they qualify, so
`config --get` goes direct while `config user.email x` does not.

`git show` blob reads stop forcing the login shell and go direct too.
The fence stays on that path: the login shell is still the fallback when
the environment probe is cold or rejected, and a banner there would
become file content.

Behavior note: the first WSL read per distro now also warms the
environment probe in the background, so a cold read spawns one extra
wsl.exe. Subsequent reads start no shell at all.
The conditional read markers were matched anywhere after the subcommand,
so a positional argument that happened to share a marker's name routed a
write shell-free: `worktree remove list` read as a listing, and
`submodule foreach status` as a status query -- the latter can run
arbitrary commands, including network ones.

Split the two kinds of marker apart. `config`/`branch` are flag-marked
and still match anywhere; `remote`/`worktree`/`submodule` are
action-marked and must match the first non-flag argument. The queried
`remote show` rule and the `symbolic-ref` arity rule are unchanged.

Neither case is reachable today -- Orca issues no `submodule foreach`,
and the worktree paths are its own CLI, not git argv -- but the loose
match was the shape of the defect, not those two instances.
@OrcaWin
OrcaWin force-pushed the OrcaWin/wsl-direct-git-for-reads branch from 5ceb5c1 to 4cdf9a9 Compare August 18, 2026 09:41
@OrcaWin
OrcaWin changed the base branch from OrcaWin/wsl-fence-git-login-shell-reads to main August 18, 2026 09:41
Writes hidden behind -c/-C/--git-dir must not reach the shell-free
route, and an unparsed global form must fall back to the login shell
rather than guess at the subcommand. Both directions fail safe.
@OrcaWin
OrcaWin force-pushed the OrcaWin/wsl-direct-git-for-reads branch from 4cdf9a9 to c401997 Compare August 18, 2026 09:42
@coderabbitai

coderabbitai Bot commented Aug 18, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds classification for shell-free WSL Git read commands, including global options and supported listing forms. It routes eligible reads through direct Git execution while retaining login-shell handling for writes, network-sensitive commands, unsupported forms, and fallback retries. New tests cover command classification and WSL routing. Existing tests now account for background environment probes, Git call ordering, and explicit undefined askpass variables.

Merge Risk: 🟡 Moderate · up to c4019

This change reroutes WSL Git reads directly, but cold-cache and direct-read failure paths can still expose login-shell output as Git data, while git-show failures may not fall back to the shell and can fail unnecessarily. The test suite also leaves a mocked platform value behind between tests, so the PR is not merge-ready until these bounded issues are fixed.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the primary change: running WSL Git reads without a login shell.
Description check ✅ Passed The description clearly explains the problem, solution, scope, linked issue, safety behavior, and testing, but omits several template headings and checkboxes.
Linked Issues check ✅ Passed The PR addresses issue #10917 by preventing shell banners from corrupting WSL Git read output while preserving login-shell fallback and environment setup.
Out of Scope Changes check ✅ Passed The production changes and tests remain focused on WSL Git read routing, fallback behavior, and regression coverage for issue #10917.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/main/git/runner.ts (2)

450-463: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fence login-shell fallbacks for direct-safe reads.

On a cold environment cache, Line 452 returns no environment. Line 460 starts the probe without waiting. Line 463 then runs the read through an unfenced login shell.

Shell startup output can therefore still prefix remote get-url output on the first request. The retry at Line 1119 has the same problem after a direct-Git failure. src/main/git/runner-wsl-read-routing.test.ts seeds the cache, so it does not cover this path.

Use captureLoginShellOutput for these login-shell fallbacks. Strip the captured markers before returning stdout. Add cold-cache and direct-failure routing tests.

Based on learnings: retry any direct-Git failure through the login shell and disable the cached direct environment only after the retry succeeds.

Also applies to: 1117-1123

Source: Learnings


1191-1206: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Retry direct buffer reads through the login shell.

If direct git show fails, this function rejects immediately. It does not retry with resolveGitCommand(args, options, true, true).

Profile-dependent Git configuration can require the login shell. Keep the existing captured-output handling, but retry direct-Git failures through the fenced login shell. Disable the cached direct environment only after that retry succeeds. Add a regression test for this path.

Based on learnings: retry any direct-Git failure through the login shell and disable the cached direct environment only after the retry succeeds.

Source: Learnings


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 642585da-9c54-4418-b62b-a2f787176dce

📥 Commits

Reviewing files that changed from the base of the PR and between b7f2e17 and c401997.

📒 Files selected for processing (6)
  • src/main/git/runner-command-exec.test.ts
  • src/main/git/runner-wsl-linked-gitdir-timeout.test.ts
  • src/main/git/runner-wsl-read-routing.test.ts
  • src/main/git/runner.ts
  • src/main/git/wsl-direct-git-read-commands.test.ts
  • src/main/git/wsl-direct-git-read-commands.ts

Included review availability: Your plan includes up to 10 reviews per rolling hour; 3 remain after this review.

Comment on lines +57 to +62
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
})

afterEach(() => {
Object.defineProperty(process, 'platform', { configurable: true, value: process.platform })
resetWslGitReadEnvironmentForTests()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Restore the original platform value.

Line 61 reads process.platform after Line 57 changes it to win32. The suite therefore leaves the mocked platform active after every test.

Store the original value before the suite starts. Restore that value in afterEach.

Proposed fix
+const originalPlatform = process.platform
+
 describe('WSL git read routing', () => {
@@
   afterEach(() => {
-    Object.defineProperty(process, 'platform', { configurable: true, value: process.platform })
+    Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform })
     resetWslGitReadEnvironmentForTests()
   })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
})
afterEach(() => {
Object.defineProperty(process, 'platform', { configurable: true, value: process.platform })
resetWslGitReadEnvironmentForTests()
const originalPlatform = process.platform
describe('WSL git read routing', () => {
// ...
afterEach(() => {
Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform })
resetWslGitReadEnvironmentForTests()
})
})

@OrcaWin
OrcaWin merged commit 6efd406 into main Aug 18, 2026
46 checks passed
@OrcaWin
OrcaWin deleted the OrcaWin/wsl-direct-git-for-reads branch August 18, 2026 09:55
paidaxingyo666 pushed a commit to paidaxingyo666/Manta that referenced this pull request Aug 21, 2026
* fix(git): run WSL git reads without a shell

WSL-routed git ran through the distro user's interactive login shell for
one reason: to inherit their PATH. That shell also runs the distro's
rc/motd and writes it to the stdout callers parse, which is why stablyai#10917
reports a shell banner breaking GitHub source detection.

A shell-free route already existed (`--exec /usr/bin/env PATH=... git`,
added for status reads in stablyai#13207) but it was opt-in, and only
gitStatusReadOptionsForWorktree opted in. Every other read -- remote
get-url, config --get, log, show, rev-parse -- took the login shell on
every call, so the reported parse never benefited.

Classify reads at the resolver instead of at each caller. A read needs
nothing the login shell provides, so it takes the direct route without
the caller asking. Writes and network operations stay on the login
shell: they can depend on credential helpers and ssh-agent that the
user's profile sets up.

Subcommands that both read and write (config, remote, branch,
submodule) require an explicit read flag before they qualify, so
`config --get` goes direct while `config user.email x` does not.

`git show` blob reads stop forcing the login shell and go direct too.
The fence stays on that path: the login shell is still the fallback when
the environment probe is cold or rejected, and a banner there would
become file content.

Behavior note: the first WSL read per distro now also warms the
environment probe in the background, so a cold read spawns one extra
wsl.exe. Subsequent reads start no shell at all.

* fix(git): keep queried WSL remote reads on login shell

* style: format WSL runner test

* fix(git): match WSL read markers positionally

The conditional read markers were matched anywhere after the subcommand,
so a positional argument that happened to share a marker's name routed a
write shell-free: `worktree remove list` read as a listing, and
`submodule foreach status` as a status query -- the latter can run
arbitrary commands, including network ones.

Split the two kinds of marker apart. `config`/`branch` are flag-marked
and still match anywhere; `remote`/`worktree`/`submodule` are
action-marked and must match the first non-flag argument. The queried
`remote show` rule and the `symbolic-ref` arity rule are unchanged.

Neither case is reachable today -- Orca issues no `submodule foreach`,
and the worktree paths are its own CLI, not git argv -- but the loose
match was the shape of the defect, not those two instances.

* test(git): pin read routing behind global options

Writes hidden behind -c/-C/--git-dir must not reach the shell-free
route, and an unparsed global form must fall back to the login shell
rather than guess at the subcommand. Both directions fail safe.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
dallascrilley pushed a commit to dallascrilley/orca that referenced this pull request Aug 27, 2026
* fix(git): run WSL git reads without a shell

WSL-routed git ran through the distro user's interactive login shell for
one reason: to inherit their PATH. That shell also runs the distro's
rc/motd and writes it to the stdout callers parse, which is why stablyai#10917
reports a shell banner breaking GitHub source detection.

A shell-free route already existed (`--exec /usr/bin/env PATH=... git`,
added for status reads in stablyai#13207) but it was opt-in, and only
gitStatusReadOptionsForWorktree opted in. Every other read -- remote
get-url, config --get, log, show, rev-parse -- took the login shell on
every call, so the reported parse never benefited.

Classify reads at the resolver instead of at each caller. A read needs
nothing the login shell provides, so it takes the direct route without
the caller asking. Writes and network operations stay on the login
shell: they can depend on credential helpers and ssh-agent that the
user's profile sets up.

Subcommands that both read and write (config, remote, branch,
submodule) require an explicit read flag before they qualify, so
`config --get` goes direct while `config user.email x` does not.

`git show` blob reads stop forcing the login shell and go direct too.
The fence stays on that path: the login shell is still the fallback when
the environment probe is cold or rejected, and a banner there would
become file content.

Behavior note: the first WSL read per distro now also warms the
environment probe in the background, so a cold read spawns one extra
wsl.exe. Subsequent reads start no shell at all.

* fix(git): keep queried WSL remote reads on login shell

* style: format WSL runner test

* fix(git): match WSL read markers positionally

The conditional read markers were matched anywhere after the subcommand,
so a positional argument that happened to share a marker's name routed a
write shell-free: `worktree remove list` read as a listing, and
`submodule foreach status` as a status query -- the latter can run
arbitrary commands, including network ones.

Split the two kinds of marker apart. `config`/`branch` are flag-marked
and still match anywhere; `remote`/`worktree`/`submodule` are
action-marked and must match the first non-flag argument. The queried
`remote show` rule and the `symbolic-ref` arity rule are unchanged.

Neither case is reachable today -- Orca issues no `submodule foreach`,
and the worktree paths are its own CLI, not git argv -- but the loose
match was the shape of the defect, not those two instances.

* test(git): pin read routing behind global options

Writes hidden behind -c/-C/--git-dir must not reach the shell-free
route, and an unparsed global form must fall back to the login shell
rather than guess at the subcommand. Both directions fail safe.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
* fix(git): run WSL git reads without a shell

WSL-routed git ran through the distro user's interactive login shell for
one reason: to inherit their PATH. That shell also runs the distro's
rc/motd and writes it to the stdout callers parse, which is why stablyai#10917
reports a shell banner breaking GitHub source detection.

A shell-free route already existed (`--exec /usr/bin/env PATH=... git`,
added for status reads in stablyai#13207) but it was opt-in, and only
gitStatusReadOptionsForWorktree opted in. Every other read -- remote
get-url, config --get, log, show, rev-parse -- took the login shell on
every call, so the reported parse never benefited.

Classify reads at the resolver instead of at each caller. A read needs
nothing the login shell provides, so it takes the direct route without
the caller asking. Writes and network operations stay on the login
shell: they can depend on credential helpers and ssh-agent that the
user's profile sets up.

Subcommands that both read and write (config, remote, branch,
submodule) require an explicit read flag before they qualify, so
`config --get` goes direct while `config user.email x` does not.

`git show` blob reads stop forcing the login shell and go direct too.
The fence stays on that path: the login shell is still the fallback when
the environment probe is cold or rejected, and a banner there would
become file content.

Behavior note: the first WSL read per distro now also warms the
environment probe in the background, so a cold read spawns one extra
wsl.exe. Subsequent reads start no shell at all.

* fix(git): keep queried WSL remote reads on login shell

* style: format WSL runner test

* fix(git): match WSL read markers positionally

The conditional read markers were matched anywhere after the subcommand,
so a positional argument that happened to share a marker's name routed a
write shell-free: `worktree remove list` read as a listing, and
`submodule foreach status` as a status query -- the latter can run
arbitrary commands, including network ones.

Split the two kinds of marker apart. `config`/`branch` are flag-marked
and still match anywhere; `remote`/`worktree`/`submodule` are
action-marked and must match the first non-flag argument. The queried
`remote show` rule and the `symbolic-ref` arity rule are unchanged.

Neither case is reachable today -- Orca issues no `submodule foreach`,
and the worktree paths are its own CLI, not git argv -- but the loose
match was the shape of the defect, not those two instances.

* test(git): pin read routing behind global options

Writes hidden behind -c/-C/--git-dir must not reach the shell-free
route, and an unparsed global form must fall back to the login shell
rather than guess at the subcommand. Both directions fail safe.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: git output is parsed from an interactive login shell (-ilc), so any shell-rc banner (fastfetch/neofetch/motd) breaks GitHub source detection

2 participants