Skip to content

fix(cli): run the git credential helper under the CLI runtime [risk:medium] - #644

Open
zxch3n wants to merge 3 commits into
mainfrom
fix/git-credential-helper-cli-runtime
Open

zxch3n wants to merge 3 commits into
mainfrom
fix/git-credential-helper-cli-runtime

Conversation

@zxch3n

@zxch3n zxch3n commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Related issue

(same-repository branch; no intake Issue)

Problem / pressure

GitHub repo Session startup fails on Macs where the desktop was launched from the Dock. The credential broker starts and the token prefetch succeeds, but the bare clone aborts:

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

surfacing to the user as turn_pre_prompt_failed.

The cause is the helper command itself. buildCredentialHelperValueForHost produced !node "<helper.cjs>" — a PATH lookup. A GUI-launched app inherits the Dock's minimal PATH, which usually has no node at all, so git could not start the helper, found no username under GIT_TERMINAL_PROMPT=0, and gave up. The credential chain was healthy the whole time; only the runtime resolution was broken.

The diagnostic path had the same bug (spawn('node', …)), so on an affected machine the probe reported a spawn error instead of the broker verdict — and on a machine that happens to have a PATH node, it would have succeeded against a runtime git never used, hiding the defect being diagnosed.

Every other CLI child — CLI/MCP, adapters, the watch worker — already resolves process.execPath + ELECTRON_RUN_AS_NODE. The git credential helper was the last one depending on ambient PATH.

Summary

  • git-credential-helper-script.ts: new formatCredentialHelperCommand(nodePath, helperPath, platform). The host value is now !"<execPath>" "<helper.cjs>"; both words are quoted (installation paths contain spaces: Lody Helper, Program Files) and on win32 backslashes become forward slashes, because git runs the ! form through its bundled MinGW bash where \ escapes rather than separates.
  • New buildCredentialHelperRuntimeEnv() returns { ELECTRON_RUN_AS_NODE: '1' } when the CLI is the Electron binary. Applied to WorktreeManager.runGit, the helper probe env, and the ACP session env in session-manager.ts.
  • runCredentialHelperProbe spawns process.execPath, never node.
  • Container helpers still use !node "…": node is on PATH inside the image and the host execPath does not exist there.
  • No second Node is embedded, nothing is symlinked into ~/.lody/bin, and no login-shell PATH resolution is added.

Visual explanation

flowchart LR
  subgraph before["before — PATH lookup"]
    G1["git clone --bare"] --> H1["credential.helper<br/>!node &quot;helper.cjs&quot;"]
    H1 -. "ENOENT: no node on GUI PATH" .-> F1(["could not read Username<br/>→ turn_pre_prompt_failed"])
  end
  subgraph after["after — CLI runtime"]
    G2["git clone --bare<br/>ELECTRON_RUN_AS_NODE=1"] --> H2["credential.helper<br/>!&quot;&lt;execPath&gt;&quot; &quot;helper.cjs&quot;"]
    H2 --> B2["broker /git-credential"] --> OK(["username + password"])
  end
Loading

Three call sites resolve the runtime, all through one module:

git-credential-helper-script.ts
├── formatCredentialHelperCommand(execPath, helperPath, platform)
│   └── buildCredentialHelperValueForHost()
│       ├── worktree-manager.ts  buildGitAuthArgs()   → -c credential.helper=…  (host clone/fetch)
│       └── session-manager.ts   prepareGitHubRepo…() → GIT_CONFIG_VALUE_1      (ACP git children)
└── buildCredentialHelperRuntimeEnv()
    ├── worktree-manager.ts  runGit()                 (host git child env)
    ├── worktree-manager.ts  diagnose…()              (probe env)
    └── session-manager.ts   prepareGitHubRepo…()     (ACP session env)

Before / after

Before After
credential.helper=!node "/Users/me/.lody/repos/<id>/lody-git-credential-helper.cjs" credential.helper=!"/Applications/Lody.app/Contents/MacOS/Lody" "/Users/me/.lody/repos/<id>/lody-git-credential-helper.cjs"
spawn('node', [helperPath, 'get']) → probe reports ENOENT, broker verdict lost spawn(process.execPath, [helperPath, 'get']) → probe reports what git would see
Windows: !node "C:\Users\dev\…\helper.cjs" (backslashes reach MinGW bash) !"C:/Program Files/Lody/Lody.exe" "C:/Users/dev/…/helper.cjs"
Git children inherit no ELECTRON_RUN_AS_NODE Forced to 1 on the git child, the probe, and the ACP session env under Electron

Test plan

  • New apps/cli/src/lib/git-credential-helper-script.test.ts runs real git credential fill against the produced helper value, with the helper under a directory containing a space and a failing node shim prepended to PATH — the Dock environment, without depending on the machine's real PATH. Reverting the fix reproduces the exact production error: fatal: could not read Username for 'https://…': terminal prompts disabled. Skipped on win32 (POSIX shim); the Windows separator rule is covered by formatting assertions instead.
  • worktree-manager-broker-auth.test.ts (owning suite, extended): host git argv names process.execPath and never !node , the probe spawns process.execPath with [helperPath, 'get'], and ELECTRON_RUN_AS_NODE is forced under Electron / absent under plain Node. Its makeChild fake now emits close only after both streams drain, so a stderr-classified failure is actually visible to the code under test.
  • session-manager.test.ts (extended): GIT_CONFIG_VALUE_1 and ELECTRON_RUN_AS_NODE on the ACP session env, with a stand-in token manager and broker.
  • tests/worktree-manager.create.test.ts: updated the hardcoded helper literal to the quoted form.
  • Every new assertion was ablated individually against pre-fix code and fails without it (the two GIT_CONFIG_VALUE_1/argv assertions are written independently of the formatter so they are not tautological).
  • pnpm --filter lody typecheck, pnpm format, pnpm run docs check (exit 0) all pass. Full pnpm check: 2721 passed, 1 failedtests/worktree-gc.test.ts fails identically on this branch's merge-base (macOS /private/var vs /var realpath), unrelated to this change.
  • Not verified: native Windows desktop startup, and a packaged Electron build on a real Dock-launched machine — only the reconstructed PATH environment.

Context handoff

Instructions for reviewing agents

  • Review focus: git-credential-helper-script.ts quoting/normalization, and that all three ELECTRON_RUN_AS_NODE call sites (host runGit, probe env, ACP session env) actually reach the child process.
  • Decisions to challenge: forcing ELECTRON_RUN_AS_NODE after the caller-supplied env spread in runGit (a caller can no longer unset it); keeping the container helper on !node; normalizing \/ only on win32 rather than unconditionally.
  • Plausible failures / evidence gaps: Windows is covered by string assertions only — the MinGW-bash path is not exercised by a real git credential fill; and a Windows execPath containing a " or a UNC prefix is escaped but untested.

Authoring context

  • User goal / directives: fix the confirmed Dock-launch GitHub-session startup failure with the smallest surgical change, in the public repo only, with tests and an OSS PR.
  • Constraints / non-goals: no embedded second Node, no ~/.lody/bin/node symlink, no login-shell PATH resolution; no changes to broker routing, token handling, or the private pointer.
  • Risk-bearing decisions: credential.helper is a shell command git executes, so its quoting is security-relevant; both words are quoted and embedded " escaped. Forcing ELECTRON_RUN_AS_NODE overrides a caller-supplied value on git children.
  • Destructive or irreversible behavior: none. No migration, no on-disk format change; the helper script content is unchanged and existing files are reused.
  • Deliberately not done or tested: no real Windows git credential fill run; no packaged-desktop end-to-end run; the container helper value is deliberately untouched.
  • Unknowns / confidence: high confidence on macOS/Linux — the regression test reproduces the exact production error string when the fix is reverted. Medium confidence on Windows, where only the formatted string is asserted.

🤖 Generated with Claude Code

…edium]

GitHub repo Sessions failed to start on Macs launched from the Dock. The
credential broker and token prefetch both succeeded, but `credential.helper`
was `!node "<helper.cjs>"` — a PATH lookup. A GUI-launched app inherits the
Dock's minimal PATH, which usually has no `node`, so the helper never ran,
git found no username under `GIT_TERMINAL_PROMPT=0`, and the clone aborted
with `terminal prompts disabled` → `turn_pre_prompt_failed`.

Build the host helper command from `process.execPath` instead, quoting both
words (installation paths contain spaces) and normalizing Windows separators
to `/`, since git runs the `!` form through its bundled MinGW bash. The
diagnostic probe spawns the same runtime rather than a bare `node`: on an
affected machine it reported a spawn error instead of the broker verdict, and
on a machine with a PATH `node` it would have succeeded against a runtime git
never used. Host git children, the probe, and the ACP session environment now
carry `ELECTRON_RUN_AS_NODE=1` when the CLI is the Electron binary. Container
helpers keep `node`, which is on PATH inside the image.

Model: claude-opus-5

@zxch3n zxch3n left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Codex review (could not --approve: GitHub rejects self-approval by zxch3n, who is also the PR author).

Reviewed PR #644 at 89d80c1 against origin/main. No blocking correctness or security findings. The host helper now uses process.execPath with both words quoted and Windows separators normalized to /; host Git, the ACP environment, and diagnostics propagate ELECTRON_RUN_AS_NODE=1 for Electron; container helpers retain !node.

Focused Vitest could not run in the review checkout because node_modules/vitest is absent; git diff --check passed.

Non-blocking test-quality notes: the WorktreeManager Electron flag test seeds process.env, which the old runGit already inherited, and worktree-manager.create only tests config-arg pass-through; neither affects runtime correctness.

PR is still draft; this review does not mark it ready.

Add the Chinese counterpart of the CLI-runtime credential helper note and mark
both sides `Translation: current` with counterpart links. Same decision, not a
rewrite; no code or Spec changes.

Model: claude-opus-5
@zxch3n
zxch3n marked this pull request as ready for review September 12, 2026 13:51
…low]

The helper-path swap does not need dedicated coverage: delete the new
`git-credential-helper-script.test.ts` and revert the expansions in
`worktree-manager-broker-auth.test.ts` and `session-manager.test.ts`. The only
remaining test edit updates the existing `!node "…helper.cjs"` literal in
`worktree-manager.create.test.ts` so the suite still passes.

`formatCredentialHelperCommand` and the injectable env/platform parameters existed
only for those tests, so they fold back into the two callers. Both notes are cut to
the problem and the fix.

Model: claude-opus-5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant