Windows: every command leaks an orphaned broker, and a live app-server makes the workspace directory undeletable

Author: y-shimomotoCreated Sep 3, 2026Updated Sep 14, 2026

Windows: every command leaks an orphaned broker, and a live app-server makes the workspace directory undeletable

Version: codex-plugin-cc 1.0.6 (db52e28) · codex-cli 0.152.1 · node v22.23.2 · Windows 11 Pro

Two separate defects that compound. Symptom: after running /codex:review from a git worktree, git worktree remove fails with Permission denied and git drops the worktree registration anyway, leaving an orphaned directory behind. This has recurred ~7 times over three weeks for us; we currently work around it by killing the process tree.


1. The broker readiness budget is unreachable on Windows, and the timeout path leaks the process

ensureBrokerSession() waits 2000 ms for the broker to accept connections:

javascript
// scripts/lib/broker-lifecycle.mjs:149
const ready = await waitForBrokerEndpoint(endpoint, options.timeoutMs ?? 2000);
if (!ready) {
  teardownBrokerSession({
    endpoint, pidFile, logFile, sessionDir,
    pid: child.pid ?? null,
    killProcess: options.killProcess ?? null   // ← always null from connect()
  });
  return null;
}

But the broker only starts listening after its own app-server client has finished initialize:

javascript
// scripts/app-server-broker.mjs:68
const appClient = await CodexAppServerClient.connect(cwd, { disableBroker: true });
...
// scripts/app-server-broker.mjs:246
server.listen(listenTarget.path);

On Windows, codex app-server is spawned through a shell (shell: process.env.SHELL || true, scripts/lib/app-server.mjs:194), so the chain is node brokerbash -c "codex app-server"bashshnode codex.jscodex.exe. Measured time until the endpoint accepts a connection, three consecutive runs on an otherwise idle machine:

ready within the shipped 2000 ms budget: false
ready within 60000 ms: true (elapsed 3617 ms)
ready within 60000 ms: true (elapsed 2984 ms)
ready within 60000 ms: true (elapsed 3424 ms)

Consequences, all observed:

  • The readiness check always times out, so ensureBrokerSession() returns null and every command silently falls back to a direct app-server. The broker feature never engages.
  • CodexAppServerClient.connect() calls ensureBrokerSession(cwd, { env: options.env }) and therefore never supplies killProcess, so the timeout path does not kill the broker. It only unlinks the pid file. The broker finishes starting a second later and runs forever.
  • saveBrokerSession() is never reached, so no broker.json is written, and the SessionEnd hook's broker/shutdown cannot find the process it should have shut down.

Net effect: one orphaned node app-server-broker.mjs plus its whole codex app-server tree leaks per companion invocation. After a six-command test sweep we counted seven live brokers; two more were still holding a worktree from the previous day.

Raising the budget to 15000 ms makes the broker work as designed — broker.json is written and the same broker is reused by the next invocation — which is how we confirmed defect 2.

2. A live app-server keeps the workspace directory open, so Windows refuses to delete it

The broker is spawned with the workspace as its OS working directory, and passes that same value to its app-server child:

javascript
// scripts/lib/broker-lifecycle.mjs:59-67
const child = spawn(process.execPath, [scriptPath, "serve", "--endpoint", endpoint, "--cwd", cwd, ...], {
  cwd, env, detached: true, ...
});

On Windows a process's working directory cannot be deleted, so while that tree is alive the worktree cannot be removed:

error: failed to delete '.../sandbox-worktrees/wt1': Permission denied

(and git removes the worktree registration regardless, so the directory is left orphaned).

Moving the OS cwd alone is not sufficient. We tested three variants against throwaway worktrees:

variant broker engaged? git worktree remove
1.0.6 as shipped no (defect 1) fails — orphaned broker tree holds it
broker's OS cwd moved, --cwd unchanged no fails — app-server child still on the worktree
broker and app-server started from the broker's sessionDir no succeeds
same, plus a 15000 ms readiness budget so the broker actually engages yes fails again

In the last row nothing on the worktree appears in any command line, yet the directory is still locked; killing the broker's process tree releases it. So once the broker is genuinely used, the workspace is held by processes codex itself keeps on the thread's cwd under the live app-server — not by the broker's own working directory.

What does release it, immediately and cleanly:

shutting down broker pid=... endpoint=pipe:\\.\pipe\cxc-...
shutdown returned after 4 ms
--- removal after shutdown ---   → succeeds

What we think the fix looks like

Defect 1 is straightforward: listen before connecting to the app-server (readiness becomes immediate), or raise the budget and pass killProcess on the timeout path so a broker that misses it is torn down instead of leaked.

Defect 2 needs the app-server for a workspace to not outlive the work. Either an idle shutdown once the last client disconnects, or a public opt-out for the broker so each command uses a direct app-server it closes at the end. Internally disableBroker and a direct path already exist (scripts/lib/app-server.mjs:338, scripts/lib/codex.mjs:645), but 1.0.6 exposes no setting or flag for it. Setting CODEX_COMPANION_APP_SERVER_ENDPOINT to an endpoint nobody listens on does reach the direct path via the ENOENT fallback in withAppServer(), and it does fix the symptom — but it also makes /codex:setup report loggedIn: false with detail: "connect ENOENT ...", so it is not usable as a workaround.

Changing the OS cwd is not the fix on its own, per the table above.

Reproduction

Windows, any git worktree:

bash
git worktree add ../wt -b topic
node scripts/codex-companion.mjs review --base main --cwd <path-to-wt>
git worktree remove ../wt      # Permission denied; registration dropped anyway