Repository navigation
fix(wsl): Codex WSL hook installs and host hook checks never start a stopped distro (E9) - #35827
Conversation
Walkthrough
Priority: Merge Risk:
|
[Medium impact] The changes affect background Codex hook work and repository hook checks on Windows WSL. The PR still needs the callback race fixed before merging; an older reconciliation can undo newer hook changes. FindingsSummaryBackground Codex hook installs and remote-host hook checks now check whether a WSL distro is running before touching its files.
|
| // The probe settled after the install; the distro may have stopped since (E9). | ||
| const distro = resolvedPlan?.wslDistro ?? wslPlan?.wslDistro | ||
| if (!distro || (await isWslHookDistroBlocked(distro))) { | ||
| return | ||
| } |
There was a problem hiding this comment.
Old callbacks undo newer changes
The new await isWslHookDistroBlocked(distro) lets a newer launch or hooks-off refresh supersede this callback after isCurrentGeneration was checked. The old callback then still removes trust or reinstalls hooks using its earlier decision. Turning hooks off during this wait can therefore be undone when the old callback resumes.
Recheck the generation after the wait and before the queued mutation.
| target, | ||
| 'repo.hooksCheck', | ||
| { repo: repoId }, | ||
| { repo: repoId, ...(options.background ? { background: true } : {}) }, |
There was a problem hiding this comment.
Setup guide saves temporary absence
Forwarding background makes runtime-host checks return unavailable for stopped or unverifiable WSL distros. But useSetupGuideProgress treats that reply as a missing setup script and saves hasSetupScript: false. Its effect does not watch distro availability, so a configured remote project can keep showing an incomplete setup step after its distro starts.
Preserve the unavailable state and retry when the host becomes available instead of recording absence.
…stopped distro (E9) (a) installManagedHooksIntoWslRuntime checks the settled running gate inside its trust queue right before the first UNC read, and again before the post-hash fallback trust write; a later reconcile checks before revoking or reinstalling. A stopped or unverifiable distro skips (null, as with no WSL plan). The canonical-path probe runs through process-host behind blockedWslDistroForExec; a refused probe settles as unavailable and keeps the known identity. The guest shell preflight and a pane launch's home preparation run as user acts on their distro. (b) repo.hooksCheck takes an optional background flag (older hosts ignore it); the renderer sends it, and the host refuses a stopped distro's background check the same way the desktop's hooks:check does.
A passive restore or remount reaches launch preparation too (#35813), so it no longer runs as a user act; a stopped distro skips the install and the relay installs once the distro runs. A skipped WSL home now returns not_installed instead of falling through to the host writer, which would have written the WSL home over UNC.
hooks.json is written before the hash wait, so a skip after it is not a clean skip: return an error status saying the trust is pending instead of null.
aa135f0 to
f3ffaf5
Compare
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

Minor · Gate the hooks-disabled WSL refresh before UNC… · codex-hook-service-implementation.ts:198-210
src/main/codex/codex-hook-service-implementation.ts:198-210
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winGate the hooks-disabled WSL refresh before UNC access.
The hooks-disabled pane path calls
refreshWslRuntimeUserHookswithout checkingisWslHookDistroBlocked. That helper reads and writeshooks.jsonand trust files. The gate contract states that any UNC touch can boot a stopped distro. An unverifiable distro is also blocked. This violates the PR's no-start/no-access invariant.Make the wrapper await the existing asynchronous gate before calling
refreshWslRuntimeUserHooks. ReturnskippedWslHookInstallStatusfor a blocked WSL plan so the caller does not fall through to host preparation.Suggested fix
- this.refreshRuntimeUserHooksForRuntimeHome(runtimeHomePath, target) ?? + (await this.refreshRuntimeUserHooksForRuntimeHome(runtimeHomePath, target)) ?? (await this.refreshRuntimeUserHooksForLaunchPrep(runtimeHomePath ?? undefined)) ) } - refreshRuntimeUserHooksForRuntimeHome( + async refreshRuntimeUserHooksForRuntimeHome( runtimeHomePath: string | null | undefined, target?: CodexWslRuntimeHookTarget - ): AgentHookInstallStatus | null { + ): Promise<AgentHookInstallStatus | null> { this.supersedeWslReconciliation(runtimeHomePath) const wslPlan = createCodexWslRuntimeHookInstallPlan(runtimeHomePath, target) + if ( + runtimeHomePath && + wslPlan && + (await isWslHookDistroBlocked(wslPlan.wslDistro)) + ) { + return skippedWslHookInstallStatus(runtimeHomePath) + } return wslPlan ? refreshWslRuntimeUserHooks(wslPlan) : null }

Minor · Preserve the unverifiable WSL state in the skipped-install… · codex-hook-wsl-runtime.ts:41-53
src/main/codex/codex-hook-wsl-runtime.ts:41-53
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winPreserve the unverifiable WSL state in the skipped-install status.
blockedWslDistroForPath('', distro)returns separatestoppedandunverifiablestates, butisWslHookDistroBlockedconverts both totrue. The skipped-install path then returns anAgentHookInstallStatuswhosedetailsays the distro is not running. An unverifiable distro can therefore produce a false operator diagnostic.Preserve the state when constructing the status, or use state-neutral wording:
Suggested fix
-/** A WSL home whose install was skipped because its distro is stopped; the relay installs later. */ +/** A WSL home whose install was skipped because its distro is unavailable; the relay installs later. */ ... - detail: 'The WSL distro is not running; Orca installs these hooks once it runs.' + detail: 'The WSL distro is unavailable; Orca installs these hooks once it is available.'

Review info
-
Run configuration-
- Source mode: Auto
- Repository: Unavailable
- Branch: Unavailable
- File: No configuration file from this repository applied
- Commit: Unavailable
- Layers: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
4618a8ad-12f0-478c-a503-2c1aa2f2c7b6
Files ignored due to path filters: 1-
src/shared/rpc-contract/rpc-params-catalog.generated.tsis excluded by!**/*.generated.*
Files selected for processing: 5-
config/process-host-direct-import-baseline.txtsrc/main/runtime/rpc/methods/repo.test.tssrc/main/runtime/rpc/methods/repo.tssrc/renderer/src/runtime/runtime-hooks-client.tssrc/shared/rpc-contract/repo-params.ts
Files with no reviewable changes: 1-
- config/process-host-direct-import-baseline.txt
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 1 remain after this review.
-
ELI5
Two background jobs could still start a stopped WSL Linux: the job that installs Orca's Codex hooks into a WSL Codex home, and the check for a project's
orca.yamlwhen that project lives on a remote Orca host. Now both check first whether the distro is running and leave it alone if it isn't. Opening a terminal or pane in that distro still installs the hooks as before.What Changed
(a) Codex WSL hook install and reconcile (
codexHookService.installForRuntimeHome, reached in the background from the WSL hook relay):hooks.json,.orca/agent-hooks/codex-hook.shandconfig.tomlunder\\wsl.localhost\<distro>\…. The canonical-path probe was a rawexecFile('wsl.exe', … readlink)with no running check. If the distro stopped during the ≤3 s hash wait, or before the readlink settled, the next UNC write started it again.installManagedHooksIntoWslRuntimeasks the shared settled checkblockedWslDistroForPath('', distro)inside the per-file queue, right before its first UNC read. If the distro is stopped or unverifiable then, it skips and returnsnull, the existing "no WSL plan" result. It asks again before the post-hash fallback trust write. By that pointhooks.jsonis already written, so a skip there returns an error status saying the trust is pending, notnull. A user act on that distro passes the check.@orca/process-host(runProcesswithwslExeProcessSpec) instead of rawexecFile, behindblockedWslDistroForExec(distro). A refused probe settles asunavailable, which keeps the last known-good trust identity, exactly like a failedwsl.exe.prepareManagedWslCodexHomeBeforeShellLaunch) runs as a user act on its distro.not_installed; it no longer falls through to the host writer, which would have written the WSL home over UNC. The hook relay installs the hooks once the distro runs.(b) Runtime-RPC hook check:
checkRuntimeHooks(…, { background: true })droppedbackgroundon the environment path, so the host'srepo.hooksCheck→checkRepoHooksreadorca.yamlover UNC with no check.repo.hooksChecktakes an optionalbackground(RepoHooksCheck=RepoSelector+background?). The renderer sends it, and the host refuses exactly as the desktop'shooks:checkIPC does:status: 'error'withunavailable: 'wsl-distro-stopped' | 'wsl-distro-unverifiable', without reading any file.Why
Both are E9 gaps found by the preflight. They reuse the shared gate (
wsl-unc-access-gate/wsl-background-exec-gate) instead of adding another rule. Checking inside the trust queue, and again after the hash wait, closes the windows the preflight named; a single check at the start would not.Linked Issue
WSL re-write, gate E9 (wsl-e9-preflight findings).
Visual Proof
N/A: no UI change.
Testing
codex-wsl-hook-install-plan.test.ts: rewritten for process-host. New: a stopped distro is never probed (runProcessnot called), the settlement isunavailable, and the cached identity is kept.codex-hook-wsl-stopped-distro.test.ts:codex-wsl-hook-user-acts.test.ts: the shell preflight runs as a user act on its own distro and no other; a pane launch does not run as one.not_installedand never calls the host writer.runtime-repository-hooks-commands.test.ts: a background check of a stopped or unverifiable distro reads nothing and says why; a user or older-client check reads as before.repo.test.ts(the RPC passesbackground) andruntime-hooks-client.test.ts(the renderer sends it to the host).pnpm run typecheck:ci, oxlint,check:code-quality:changedandverify:rpc-params-catalogpass.Review
Agent skill upstream boundary
Notes
backgroundand behaves as before. An old client never sends it.backgroundis a new optional param onrepo.hooksCheck. The response can carry the existing optionalunavailablefield.