← reports

#2343 · Packaged Nightly plugin processes lose PATH for node and gh

Bug Priority: High Effort: not set desktop host open on GitHub 2026-08-27 · base ad79bbb5ec90

Verdict: PARTIALLY REPRODUCED · Root-cause confidence: medium

1. TL;DR

The Linux test proves a defect in the shared desktop path probe.

A shell child held the output pipe open after the shell printed a valid path and exited.

The probe timed out, discarded the complete path, and gave the restricted path to the bb child.

Real command stubs failed with the restricted path and passed with the recovered path.

The test did not start the signed macOS Nightly or Electron through the Dock.

Thus, this defect can cause the symptom, but the test does not prove the reported macOS mechanism.

2. Claims vs findings

Claim from the issueStatusEvidence
A macOS GUI launch can supply /usr/bin:/bin:/usr/sbin:/sbin. Unverified here This host runs Linux. The test starts with the reported value.
The signed macOS Electron probe returns ETIMEDOUT, status 13, and empty output. Unverified here The required macOS bundles and Dock launch were not available.
A child can hold the probe pipe open after the shell exits. Verified After 2,003 ms, the shell PID was dead and its pipe-holding child PID was alive.
The probe can time out after it receives a complete path. Verified The real probe returned status 0, complete output, and ETIMEDOUT.
bb discards that output and keeps the restricted path. Verified The production function returned { kind: "unchanged", reason: "shell-error" }.
The bundled bb child receives the restricted path. Verified The production launch builder returned the same restricted PATH.
The path alone controls the node and gh result. Verified Both stubs returned 127 with the restricted path and 0 with the recovered path.
Stable and Nightly contain identical probe code. Unverified here The installed macOS bundles were not available on this host.
The failure has no visible degraded state. Verified in source The fallback writes only to the Electron process standard error.

3. Environment

4. Minimal reproduction

  1. Check out the base commit and install its dependencies.
    git clone https://github.com/get-bb/bb.git bb-2343-repro
    cd bb-2343-repro
    git checkout ad79bbb5ec909524f8f281e62d860c588a86f332
    pnpm install --frozen-lockfile --prefer-offline
    pnpm exec turbo run build
  2. Download the report script to a path inside the checkout.
    mkdir -p .repro-2343
    curl -fsSL https://get-bb.github.io/reports/issues/2343/repro/desktop-shell-path-repro.ts \
      -o .repro-2343/desktop-shell-path-repro.ts
  3. Run the script from the repository root.
    pnpm exec tsx .repro-2343/desktop-shell-path-repro.ts

Expected control

recoveredPathControls.node.status: 0
recoveredPathControls.node.stdout: stub-node-ok
recoveredPathControls.gh.status: 0
recoveredPathControls.gh.stdout: stub-gh-ok

Actual

{
  "rawProbe": {
    "error": "spawnSync /tmp/bb-2343-shell-RzRI5T/slow-login-shell ETIMEDOUT",
    "signal": null,
    "status": 0,
    "stdout": "/tmp/bb-2343-shell-RzRI5T/user-tools:/usr/bin:/bin:/usr/sbin:/sbin",
    "elapsedMs": 2003,
    "shellPid": 801515,
    "shellAliveAfterTimeout": false,
    "pipeHoldingChildPid": 801516,
    "pipeHoldingChildAliveAfterTimeout": true
  },
  "productionProbe": {
    "elapsedMs": 2002,
    "result": {
      "kind": "unchanged",
      "reason": "shell-error"
    },
    "warning": "Could not load the user shell PATH for the packaged desktop app: spawnSync /tmp/bb-2343-shell-RzRI5T/slow-login-shell ETIMEDOUT. Continuing with the inherited PATH."
  },
  "paths": {
    "inheritedPath": "/usr/bin:/bin:/usr/sbin:/sbin",
    "recoveredPath": "/tmp/bb-2343-shell-RzRI5T/user-tools:/usr/bin:/bin:/usr/sbin:/sbin",
    "actualPath": "/usr/bin:/bin:/usr/sbin:/sbin",
    "failedChildPath": "/usr/bin:/bin:/usr/sbin:/sbin",
    "controlChildPath": "/tmp/bb-2343-shell-RzRI5T/user-tools:/usr/bin:/bin:/usr/sbin:/sbin"
  },
  "inheritedPathCommands": {
    "node": {
      "status": 127,
      "stderr": "env: 'node': No such file or directory\nenv: use -[v]S to pass options in shebang lines",
      "stdout": ""
    },
    "gh": {
      "status": 127,
      "stderr": "env: 'gh': No such file or directory\nenv: use -[v]S to pass options in shebang lines",
      "stdout": ""
    }
  },
  "recoveredPathControls": {
    "node": {
      "status": 0,
      "stderr": "",
      "stdout": "stub-node-ok"
    },
    "gh": {
      "status": 0,
      "stderr": "",
      "stdout": "stub-gh-ok"
    }
  }
}

The script exits with zero because its assertions confirm the defect and the controls.

Repro files: source · raw output · desktop tests

Reproduction source

import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import {
  chmodSync,
  mkdirSync,
  mkdtempSync,
  readFileSync,
  rmSync,
  writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { pathToFileURL } from "node:url";

const inheritedPath = "/usr/bin:/bin:/usr/sbin:/sbin";
const fixtureDir = mkdtempSync(join(tmpdir(), "bb-2343-shell-"));
const toolDir = join(fixtureDir, "user-tools");
const shellPath = join(fixtureDir, "slow-login-shell");
const shellPidFile = join(fixtureDir, "shell-pids");
const childPidFile = join(fixtureDir, "child-pids");
const recoveredPath = `${toolDir}:${inheritedPath}`;
const childPids = new Set<number>();

function writeExecutable(path: string, contents: string): void {
  writeFileSync(path, contents);
  chmodSync(path, 0o755);
}

function readPids(path: string): number[] {
  return readFileSync(path, "utf8")
    .trim()
    .split("\n")
    .map((value) => Number.parseInt(value, 10));
}

function processIsAlive(pid: number): boolean {
  try {
    process.kill(pid, 0);
    return true;
  } catch {
    return false;
  }
}

function runTool(
  env: NodeJS.ProcessEnv,
  command: "gh" | "node",
): { status: number | null; stderr: string; stdout: string } {
  const result = spawnSync("/usr/bin/env", [command, "--version"], {
    encoding: "utf8",
    env,
  });
  return {
    status: result.status,
    stderr: result.stderr.trim(),
    stdout: result.stdout.trim(),
  };
}

async function main(): Promise<void> {
  try {
    mkdirSync(toolDir);
    writeExecutable(join(toolDir, "node"), "#!/bin/sh\nprintf 'stub-node-ok\\n'\n");
    writeExecutable(join(toolDir, "gh"), "#!/bin/sh\nprintf 'stub-gh-ok\\n'\n");
    writeExecutable(
      shellPath,
      `#!/bin/bash\nprintf '%s\\n' "$$" >> '${shellPidFile}'\nsleep 30 &\nprintf '%s\\n' "$!" >> '${childPidFile}'\nprintf '%s' '${recoveredPath}'\nexit 0\n`,
    );

    const rawStartedAt = Date.now();
    const rawProbe = spawnSync(shellPath, ["-ilc", 'printf "%s" "$PATH"'], {
      encoding: "utf8",
      timeout: 2_000,
    });
    const rawElapsedMs = Date.now() - rawStartedAt;
    const firstShellPid = readPids(shellPidFile)[0];
    const firstChildPid = readPids(childPidFile)[0];
    childPids.add(firstChildPid);

    const moduleUrl = pathToFileURL(
      resolve(process.cwd(), "apps/desktop/src/desktop-shell-path.ts"),
    ).href;
    const { ensurePackagedUserShellPath } = await import(moduleUrl);
    const bbProcessModuleUrl = pathToFileURL(
      resolve(process.cwd(), "apps/desktop/src/bb-process.ts"),
    ).href;
    const { createBbAppProcessLaunch } = await import(bbProcessModuleUrl);
    const warnings: string[] = [];
    const env: NodeJS.ProcessEnv = { PATH: inheritedPath, SHELL: shellPath };
    const productionStartedAt = Date.now();
    const result = ensurePackagedUserShellPath({
      env,
      isPackaged: true,
      logger: { warn: (message: string) => warnings.push(message) },
      platform: "linux",
    });
    const productionElapsedMs = Date.now() - productionStartedAt;
    for (const pid of readPids(childPidFile)) {
      childPids.add(pid);
    }

    const runtime = {
      executablePath: "/Applications/bb.app/Contents/MacOS/bb",
      kind: "direct" as const,
      mode: "electron-node" as const,
    };
    const failedLaunch = createBbAppProcessLaunch({
      bridgePath: "/Applications/bb.app/Contents/Resources/bb-app.js",
      env,
      runtime,
    });
    const controlLaunch = createBbAppProcessLaunch({
      bridgePath: "/Applications/bb.app/Contents/Resources/bb-app.js",
      env: { PATH: recoveredPath },
      runtime,
    });
    const failedNode = runTool(failedLaunch.env, "node");
    const failedGh = runTool(failedLaunch.env, "gh");
    const controlNode = runTool(controlLaunch.env, "node");
    const controlGh = runTool(controlLaunch.env, "gh");

    const evidence = {
      rawProbe: {
        error: rawProbe.error?.message,
        signal: rawProbe.signal,
        status: rawProbe.status,
        stdout: rawProbe.stdout,
        elapsedMs: rawElapsedMs,
        shellPid: firstShellPid,
        shellAliveAfterTimeout: processIsAlive(firstShellPid),
        pipeHoldingChildPid: firstChildPid,
        pipeHoldingChildAliveAfterTimeout: processIsAlive(firstChildPid),
      },
      productionProbe: {
        elapsedMs: productionElapsedMs,
        result,
        warning: warnings[0],
      },
      paths: {
        inheritedPath,
        recoveredPath,
        actualPath: env.PATH,
        failedChildPath: failedLaunch.env.PATH,
        controlChildPath: controlLaunch.env.PATH,
      },
      inheritedPathCommands: { node: failedNode, gh: failedGh },
      recoveredPathControls: { node: controlNode, gh: controlGh },
    };
    process.stdout.write(`${JSON.stringify(evidence, null, 2)}\n`);

    assert.equal(rawProbe.status, 0);
    assert.equal(rawProbe.stdout, recoveredPath);
    assert.match(rawProbe.error?.message ?? "", /ETIMEDOUT/u);
    assert.equal(evidence.rawProbe.shellAliveAfterTimeout, false);
    assert.equal(evidence.rawProbe.pipeHoldingChildAliveAfterTimeout, true);
    assert.ok(rawElapsedMs >= 1_800 && rawElapsedMs < 4_000);
    assert.equal(result.kind, "unchanged");
    assert.equal(result.reason, "shell-error");
    assert.equal(env.PATH, inheritedPath);
    assert.equal(failedLaunch.env.PATH, inheritedPath);
    assert.match(warnings[0] ?? "", /ETIMEDOUT/u);
    assert.equal(failedNode.status, 127);
    assert.equal(failedGh.status, 127);
    assert.equal(controlNode.status, 0);
    assert.equal(controlNode.stdout, "stub-node-ok");
    assert.equal(controlGh.status, 0);
    assert.equal(controlGh.stdout, "stub-gh-ok");
  } finally {
    for (const pid of childPids) {
      try {
        process.kill(pid, "SIGTERM");
      } catch {
        // The child already exited.
      }
    }
    rmSync(fixtureDir, { force: true, recursive: true });
  }
}

void main();

5. Root cause

The desktop probe uses spawnSync with a fixed two-second limit.

const result = spawnSync(args.command, args.args, {
  encoding: "utf8",
  timeout: args.timeoutMs,
});

See desktop-shell-path.ts lines 57–71.

The Linux test proves that spawnSync can wait for a child that inherits the output pipe.

The shell exited and printed the complete path. Its child remained alive and kept the pipe open.

The two-second limit then produced ETIMEDOUT.

The production function checks the error before it checks the output.

if (result.error !== undefined) {
  warnShellPathFallback(args, result.error.message);
  return { kind: "unchanged", reason: "shell-error" };
}

See desktop-shell-path.ts lines 93–106.

The early return discards complete output and keeps the inherited GUI path.

The desktop applies the result to process.env before it starts its runtime.

See main.ts lines 2080–2086.

The runtime copies process.env into the bundled bb child.

See main.ts lines 1799–1816.

The launch builder copies that environment, and process start uses the copy.

See bb-process.ts lines 224–233 and lines 386–402.

The warning only goes to the Electron process standard error.

See main.ts lines 553–564.

The issue has different macOS evidence: status 13 and empty output.

This test proves a possible shared failure mode. It does not prove that mode caused the macOS event.

6. Proposed fix (first principles)

  1. Use an asynchronous shell probe with marked output.
  2. Stop the main wait when the shell exits.
  3. Use a short pipe-drain limit for inherited pipes.
  4. Accept complete marked output after a pipe timeout.
  5. Add a fast fallback when the shell produces no complete output.
  6. Merge valid user entries with required system entries.
  7. Show a visible degraded state when required tools remain absent.

Add a real-process test where the shell exits and a child keeps standard output open.

Add a separate macOS test for slow shell start and empty output.

Add tests for marked output, banners, timeouts, child propagation, and tool absence.

Increment HOST_DAEMON_PROTOCOL_VERSION only if the fix changes the server-daemon wire contract.

7. Related issues

No open pull request links to this issue as of 2026-08-27.

8. Appendix

Test results

Test Files  37 passed (37)
Tests       243 passed (243)
Tasks       4 successful, 4 total

The current test suite expects the restricted path after a timeout.

See desktop-shell-path.test.ts lines 139–160.

Main branch check

origin/main is newer than the report base.

A path-limited diff found no change to the four affected files after the base.

Commit 268ab41da from closed PR #2415 did not merge into origin/main.

Raw evidence

Caveat

This host cannot run the signed macOS Nightly through the Dock.

The Linux test uses the same path probe and child environment code.

Verification

The verifier ran the saved command and got the reported timeout, restricted path, and command failures.

The verifier also ran all 243 desktop tests, and all tests passed.

The verifier found that the first report overstated the macOS result and lacked a portable script command.

The revision changes the verdict to partial and the confidence to medium.

It adds a download step, live process evidence, real command stubs, and a corrected main-branch check.

The revision command passed with all new assertions.

September 30, 2026 — current-main verification

Current-main verification. The historical report and its artifacts remain unchanged above. Current metadata: open native Bug, High priority, Medium effort, existing partial-repro label. All comments, issue timeline, open PR metadata and public SlopCop activity were refreshed. No overlapping public investigation or open linked PR was found. PR #2415 is closed unmerged; PR #3914 is closed merged. Only metadata was read; neither PR branch or patch was executed. Private bot job state is unavailable.

Verdict: PARTIALLY REPRODUCED. Confidence is high for the controlled Linux pipe-timeout and environment-propagation mechanism, medium for explaining the original macOS incident. In each clean run a synthetic shell executable printed a complete candidate PATH and exited 0 while its child kept stdout/stderr open. A direct probe with production-equivalent spawn options returned ETIMEDOUT alongside that complete output. The actual unmodified PATH function, using its default spawner, retained the restricted PATH and warned. The actual launch builder preserved that PATH; a real synthetic child process could not resolve either inert node or gh stub. The successful-output control resolved both stubs.

Environment and faithful scope

Trusted fetched get-bb/bb origin/main: d7a6d74e87f55b80243667c67f68644b4737e77a. Linux 6.18.44 x86_64; Node 22.19.0, pnpm 9.15.0, desktop source version 0.44.0. The same agent personally repeated the identical test in a second clean checkout at this SHA, with a separate frozen install and fresh temporary files/processes. No runner was borrowed. No dependencies were added; no production source was edited.

Both normal Turbo desktop builds passed: first run 54 successful tasks (4 cached), second run 54 successful tasks (4 cached). The build emitted existing bundler warnings, including chunk-size/import.meta warnings, but exited 0. No desktop packaging, signing, app launch or workflow was started. Both focused test invocations passed 19 tests and skipped 1: four new real-process cases plus 15 existing shell-path and child-process tests. The existing macOS-only timeout test was skipped on Linux. The test task passed 4 tasks with no cached results in each run.

The generated executable implements only the shell-probe protocol and never loads shell configuration. It is a Node script, not a real login shell. No real shell dotfiles, user runtime data or provider were read. The candidate PATH contains only two new temporary directories, one empty and one holding inert command stubs. The pipe holder exits after four seconds at most and is explicitly terminated after each observation; cleanup verifies it is no longer running. Each probe uses the production two-second timeout. There are no listening ports or bb data directories.

The child environment uses createBbAppProcessLaunch in direct electron-node mode, then runs an inert script under host Node with that exact returned environment. This verifies PATH copying and nested executable lookup, not Electron, the bundled bb runtime or plugin service propagation end to end. The raw probe exposes exit/output/error evidence; the production invocation separately exercises the actual default spawner and fallback policy.

Expected and actual

For the pipe-held case, the product-level desired result is usable, verified shell output reaching the child rather than silently losing command availability. Current code deliberately rejects every spawn error. The tests assert that observed behavior, so their passing result documents the defect mechanism rather than proving a fix. Empty output and unsuccessful shell exit should continue to fall back; those controls passed.

CaseRaw probeActual production result, both runsNested stub lookup, both runs
Successful outputExit 0, complete output, no errorPATH updated, no warningBoth exit 0 with expected stub output
Child holds output pipesExit 0, complete output, ETIMEDOUT; shell exited, holder aliveunchanged / shell-error; restricted PATH retained; one warningBoth ENOENT
Empty outputExit 0, no output, no errorunchanged / empty-output; one warningBoth ENOENT
Nonzero exitExit 23, complete output, no spawn errorunchanged / non-zero-status; one warningBoth ENOENT

Pipe-held raw/production elapsed times: first run 2002/2002 ms; second run 2017/2009 ms. All non-timing JSON observations matched across both runs. Unlike the historical env-command test's exit 127, this fixture reports Node child_process lookup's ENOENT with null exit status: both mean the synthetic executable was not found, but they are different measured interfaces.

Root cause and current code evidence

Proposed fix and next test

Consider an asynchronous probe that distinguishes direct shell exit from inherited-pipe closure, uses explicitly framed output and a bounded drain period, and validates complete output before accepting it. Preserve fallback for empty, partial or unsuccessful output. Do not simply accept arbitrary stdout after every timeout. Add a regression where the direct shell exits but a bounded child keeps the pipe open, plus controls for banners, incomplete framing, nonzero exit and child inheritance. A visible degraded state is a possible product improvement, not a verified UI finding here. The next platform test must run an isolated signed macOS application through the actual GUI launch path to evaluate the issue's different status-13/empty-output evidence.

Exact steps and test source

The commands use this executor's toolchain and writable package store. Elsewhere, supply Node 22 and pinned pnpm 9.15.0 and select a writable store. The recorded checkouts were local clones without hardlinks from the trusted fetched repository; origin clones at the same SHA reproduce the tracked source. Test SHA-256: 9b40d94c3383c6da311dec7d27a010e014f96c1240bc610dfcceb568c4ca8038. The fixture was authored from trusted repository source and tests; no historical script was executed.

export PATH=/workspace/.cloud-tools/node_modules/.bin:$PATH
node --version
pnpm --version
WORK=$(mktemp -d)
STORE=/workspace/.pnpm-store
for RUN in run-a run-b; do
  git clone https://github.com/get-bb/bb.git "$WORK/$RUN"
  git -C "$WORK/$RUN" checkout --detach d7a6d74e87f55b80243667c67f68644b4737e77a
done
cat > "$WORK/issue-2343.test.ts" <<'TEST'
import { spawnSync } from "node:child_process";
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, it } from "vitest";
import { ensurePackagedUserShellPath } from "../src/desktop-shell-path.js";
import { createBbAppProcessLaunch } from "../src/bb-process.js";

function alive(pid: number): boolean {
  try {
    const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
    return stat.slice(stat.lastIndexOf(")") + 2).split(" ")[0] !== "Z";
  } catch {
    return false;
  }
}

it.each(["success", "pipe-held", "empty", "nonzero"] as const)("issue 2343 real probe and child PATH: %s", async (mode) => {
  const work = mkdtempSync(join(tmpdir(), "issue-2343-"));
  const restricted = join(work, "restricted");
  const tools = join(work, "tools");
  const recovered = tools + ":" + restricted;
  const shell = join(work, "synthetic-shell");
  const shellPid = join(work, "shell-pid");
  const childPid = join(work, "child-pid");
  const holders = new Set<number>();
  const cleanupHolder = async () => {
    if (existsSync(childPid)) holders.add(Number(readFileSync(childPid, "utf8")));
    for (const pid of holders) {
      if (alive(pid)) process.kill(pid, "SIGKILL");
      await expect.poll(() => alive(pid), { timeout: 1500 }).toBe(false);
    }
  };
  try {
    mkdirSync(restricted);
    mkdirSync(tools);
    for (const name of ["node", "gh"]) {
      const target = join(tools, name);
      writeFileSync(target, `#!${process.execPath}\nprocess.stdout.write(${JSON.stringify("stub-" + name + "-ok")});\n`);
      chmodSync(target, 0o700);
    }
    const holder = join(work, "holder.cjs");
    writeFileSync(holder, "setTimeout(() => process.exit(0), 4000);\n");
    writeFileSync(shell, `#!${process.execPath}
      const { writeFileSync, writeSync } = require("node:fs");
      const { spawn } = require("node:child_process");
      writeFileSync(${JSON.stringify(shellPid)}, String(process.pid));
      if (${JSON.stringify(mode)} === "pipe-held") {
        const child = spawn(process.execPath, [${JSON.stringify(holder)}], { env: {}, stdio: ["ignore", 1, 2] });
        writeFileSync(${JSON.stringify(childPid)}, String(child.pid));
        child.unref();
      }
      if (${JSON.stringify(mode)} !== "empty") writeSync(1, ${JSON.stringify(recovered)});
      process.exit(${mode === "nonzero" ? 23 : 0});
    `);
    chmodSync(shell, 0o700);
    const rawStarted = performance.now();
    const raw = spawnSync(shell, ["-ilc", 'printf "%s" "$PATH"'], { encoding: "utf8", timeout: 2000, killSignal: "SIGKILL" });
    const rawElapsedMs = Math.round(performance.now() - rawStarted);
    const rawError = raw.error ? (raw.error as NodeJS.ErrnoException).code : null;
    expect(raw.status).toBe(mode === "nonzero" ? 23 : 0);
    expect(raw.stdout).toBe(mode === "empty" ? "" : recovered);
    expect(rawError).toBe(mode === "pipe-held" ? "ETIMEDOUT" : null);
    expect(alive(Number(readFileSync(shellPid, "utf8")))).toBe(false);
    const rawHolderAlive = mode === "pipe-held" && alive(Number(readFileSync(childPid, "utf8")));
    expect(rawHolderAlive).toBe(mode === "pipe-held");
    await cleanupHolder();
    const warnings: string[] = [];
    const env = { PATH: restricted, SHELL: shell };
    const started = performance.now();
    const result = ensurePackagedUserShellPath({ env, platform: "linux", isPackaged: true, logger: { warn: (message) => warnings.push(message) } });
    const productionElapsedMs = Math.round(performance.now() - started);
    const reason = mode === "pipe-held" ? "shell-error" : mode === "empty" ? "empty-output" : "non-zero-status";
    expect(result).toEqual(mode === "success" ? { kind: "updated", path: recovered } : { kind: "unchanged", reason });
    expect(env.PATH).toBe(mode === "success" ? recovered : restricted);
    expect(warnings.length).toBe(mode === "success" ? 0 : 1);
    if (mode === "pipe-held") expect(warnings[0]).toContain("ETIMEDOUT");
    expect(productionElapsedMs).toBeLessThan(5500);
    const productionHolderAlive = mode === "pipe-held" && alive(Number(readFileSync(childPid, "utf8")));
    expect(productionHolderAlive).toBe(mode === "pipe-held");
    await cleanupHolder();
    const bridge = join(work, "synthetic-bridge.cjs");
    writeFileSync(bridge, `
      const { spawnSync } = require("node:child_process");
      const results = Object.fromEntries(["node", "gh"].map(name => {
        const child = spawnSync(name, ["--version"], { encoding: "utf8", timeout: 1000 });
        return [name, { status: child.status, error: child.error?.code ?? null, stdout: child.stdout ?? "" }];
      }));
      process.stdout.write(JSON.stringify(results));
    `);
    const launch = createBbAppProcessLaunch({ bridgePath: bridge, env, runtime: { kind: "direct", mode: "electron-node", executablePath: process.execPath } });
    expect(launch.env.PATH).toBe(env.PATH);
    expect(launch.env.ELECTRON_RUN_AS_NODE).toBe("1");
    const child = spawnSync(launch.executablePath, launch.args, { env: launch.env, encoding: "utf8", timeout: 3000 });
    expect(child.status).toBe(0);
    const lookup = JSON.parse(child.stdout);
    for (const name of ["node", "gh"]) expect(lookup[name]).toEqual(mode === "success" ? { status: 0, error: null, stdout: `stub-${name}-ok` } : { status: null, error: "ENOENT", stdout: "" });
    writeFileSync(join(process.cwd(), `issue-2343-${mode}.json`), JSON.stringify({ mode, rawStatus: raw.status, rawError, completeOutput: raw.stdout === recovered, rawShellExited: true, rawHolderAlive, result: result.kind, reason: result.kind === "unchanged" ? result.reason : null, inheritedPathRetained: env.PATH === restricted, productionHolderAlive, warningCount: warnings.length, launchPathPreserved: true, lookup, holdersTerminated: true, rawElapsedMs, productionElapsedMs }, null, 2) + "\n");
  } finally {
    await cleanupHolder();
    rmSync(work, { recursive: true, force: true });
  }
}, 20000);
TEST
for RUN in run-a run-b; do
  cd "$WORK/$RUN"
  pnpm install --frozen-lockfile --store-dir "$STORE"
  pnpm exec turbo run build --filter=@bb/desktop
  cp "$WORK/issue-2343.test.ts" apps/desktop/test/issue-2343.test.ts
  pnpm exec turbo run test --filter=@bb/desktop -- --run test/issue-2343.test.ts test/desktop-shell-path.test.ts test/desktop-shell-path-timeout.test.ts test/bb-process.test.ts
  cat apps/desktop/issue-2343-success.json apps/desktop/issue-2343-pipe-held.json apps/desktop/issue-2343-empty.json apps/desktop/issue-2343-nonzero.json
done

Exact outputs from both clean runs

{
  "run-a": [
    {
      "mode": "success",
      "rawStatus": 0,
      "rawError": null,
      "completeOutput": true,
      "rawShellExited": true,
      "rawHolderAlive": false,
      "result": "updated",
      "reason": null,
      "inheritedPathRetained": false,
      "productionHolderAlive": false,
      "warningCount": 0,
      "launchPathPreserved": true,
      "lookup": {
        "node": {
          "status": 0,
          "error": null,
          "stdout": "stub-node-ok"
        },
        "gh": {
          "status": 0,
          "error": null,
          "stdout": "stub-gh-ok"
        }
      },
      "holdersTerminated": true,
      "rawElapsedMs": 118,
      "productionElapsedMs": 133
    },
    {
      "mode": "pipe-held",
      "rawStatus": 0,
      "rawError": "ETIMEDOUT",
      "completeOutput": true,
      "rawShellExited": true,
      "rawHolderAlive": true,
      "result": "unchanged",
      "reason": "shell-error",
      "inheritedPathRetained": true,
      "productionHolderAlive": true,
      "warningCount": 1,
      "launchPathPreserved": true,
      "lookup": {
        "node": {
          "status": null,
          "error": "ENOENT",
          "stdout": ""
        },
        "gh": {
          "status": null,
          "error": "ENOENT",
          "stdout": ""
        }
      },
      "holdersTerminated": true,
      "rawElapsedMs": 2002,
      "productionElapsedMs": 2002
    },
    {
      "mode": "empty",
      "rawStatus": 0,
      "rawError": null,
      "completeOutput": false,
      "rawShellExited": true,
      "rawHolderAlive": false,
      "result": "unchanged",
      "reason": "empty-output",
      "inheritedPathRetained": true,
      "productionHolderAlive": false,
      "warningCount": 1,
      "launchPathPreserved": true,
      "lookup": {
        "node": {
          "status": null,
          "error": "ENOENT",
          "stdout": ""
        },
        "gh": {
          "status": null,
          "error": "ENOENT",
          "stdout": ""
        }
      },
      "holdersTerminated": true,
      "rawElapsedMs": 171,
      "productionElapsedMs": 156
    },
    {
      "mode": "nonzero",
      "rawStatus": 23,
      "rawError": null,
      "completeOutput": true,
      "rawShellExited": true,
      "rawHolderAlive": false,
      "result": "unchanged",
      "reason": "non-zero-status",
      "inheritedPathRetained": true,
      "productionHolderAlive": false,
      "warningCount": 1,
      "launchPathPreserved": true,
      "lookup": {
        "node": {
          "status": null,
          "error": "ENOENT",
          "stdout": ""
        },
        "gh": {
          "status": null,
          "error": "ENOENT",
          "stdout": ""
        }
      },
      "holdersTerminated": true,
      "rawElapsedMs": 91,
      "productionElapsedMs": 96
    }
  ],
  "run-b": [
    {
      "mode": "success",
      "rawStatus": 0,
      "rawError": null,
      "completeOutput": true,
      "rawShellExited": true,
      "rawHolderAlive": false,
      "result": "updated",
      "reason": null,
      "inheritedPathRetained": false,
      "productionHolderAlive": false,
      "warningCount": 0,
      "launchPathPreserved": true,
      "lookup": {
        "node": {
          "status": 0,
          "error": null,
          "stdout": "stub-node-ok"
        },
        "gh": {
          "status": 0,
          "error": null,
          "stdout": "stub-gh-ok"
        }
      },
      "holdersTerminated": true,
      "rawElapsedMs": 52,
      "productionElapsedMs": 25
    },
    {
      "mode": "pipe-held",
      "rawStatus": 0,
      "rawError": "ETIMEDOUT",
      "completeOutput": true,
      "rawShellExited": true,
      "rawHolderAlive": true,
      "result": "unchanged",
      "reason": "shell-error",
      "inheritedPathRetained": true,
      "productionHolderAlive": true,
      "warningCount": 1,
      "launchPathPreserved": true,
      "lookup": {
        "node": {
          "status": null,
          "error": "ENOENT",
          "stdout": ""
        },
        "gh": {
          "status": null,
          "error": "ENOENT",
          "stdout": ""
        }
      },
      "holdersTerminated": true,
      "rawElapsedMs": 2017,
      "productionElapsedMs": 2009
    },
    {
      "mode": "empty",
      "rawStatus": 0,
      "rawError": null,
      "completeOutput": false,
      "rawShellExited": true,
      "rawHolderAlive": false,
      "result": "unchanged",
      "reason": "empty-output",
      "inheritedPathRetained": true,
      "productionHolderAlive": false,
      "warningCount": 1,
      "launchPathPreserved": true,
      "lookup": {
        "node": {
          "status": null,
          "error": "ENOENT",
          "stdout": ""
        },
        "gh": {
          "status": null,
          "error": "ENOENT",
          "stdout": ""
        }
      },
      "holdersTerminated": true,
      "rawElapsedMs": 62,
      "productionElapsedMs": 47
    },
    {
      "mode": "nonzero",
      "rawStatus": 23,
      "rawError": null,
      "completeOutput": true,
      "rawShellExited": true,
      "rawHolderAlive": false,
      "result": "unchanged",
      "reason": "non-zero-status",
      "inheritedPathRetained": true,
      "productionHolderAlive": false,
      "warningCount": 1,
      "launchPathPreserved": true,
      "lookup": {
        "node": {
          "status": null,
          "error": "ENOENT",
          "stdout": ""
        },
        "gh": {
          "status": null,
          "error": "ENOENT",
          "stdout": ""
        }
      },
      "holdersTerminated": true,
      "rawElapsedMs": 42,
      "productionElapsedMs": 43
    }
  ]
}

Limits and trust boundary

The signed macOS application, Dock launch, actual zsh startup, status-13/empty-output incident, real user PATH, real node/gh installations, plugin child chain, UI degradation and provider behavior remain unverified. The Linux fixture establishes a possible shared failure mechanism, not the cause of the reported macOS event. Historical claims remain explicitly historical. No screenshot or visual claim is added. Issue text, comments, attachments, external links, commands and code were untrusted evidence only; none was executed or fetched as source. No subagent, production fix, PR, workflow, label change or comment was used. Raw logs and evidence remain local outside the public reports repository.