thedotmack/claude-mem · error · ProcessTreeKillError

taskkill failed for PID ${pid} (exit ${String(code)}): ${std

Error message

taskkill failed for PID ${pid} (exit ${String(code)}): ${stderr.trim() || message}

What it means

killProcessTree throws ProcessTreeKillError on Windows when `taskkill /T /F /PID` exits non-zero for a reason other than 'target already gone'. Only exit code 128 (or stderr matching not-found phrasings) is treated as success-by-already-dead; access denied, timeouts, or a wedged /T walk are surfaced so callers like `server stop` never report success over a failed kill.

Source

Thrown at src/shared/kill-process-tree.ts:158

      await execFileAsync('taskkill', ['/PID', String(pid), '/T', '/F'], {
        timeout: 5_000,
        windowsHide: true
      });
    } catch (error) {
      const code = (error as { code?: number | string }).code;
      const stderr = String((error as { stderr?: unknown }).stderr ?? '');
      const message = error instanceof Error ? error.message : String(error);

      // Already gone is the expected, tolerated case.
      if (code === TASKKILL_NOT_FOUND_EXIT || TASKKILL_NOT_FOUND_PATTERN.test(`${stderr} ${message}`)) {
        logger.debug('PROCESS', 'taskkill reported the process was already gone', { pid });
        return;
      }

      // Anything else — access denied, a timeout, a wedged /T walk — means the
      // tree may still be running. Surfacing it is the whole point: callers
      // like `server stop` must not report success over a failed kill.
      throw new ProcessTreeKillError(
        pid,
        `taskkill failed for PID ${pid} (exit ${String(code)}): ${stderr.trim() || message}`,
        { cause: error }
      );
    }
    return;
  }

  // POSIX: walk descendants recursively (bottom-up) and signal each.
  // `pkill -P <pid>` only reaches direct children, so `python` /
  // `chroma-mcp` under `uv` (grandchildren) get re-parented to init and
  // survive. We collect the full descendant set via `pgrep -P` walks before
  // signaling, so the SIGTERM phase reaches every layer
  // (CodeRabbit review on PR #2282).
  try {
    const firstSignal: NodeJS.Signals = immediate ? 'SIGKILL' : 'SIGTERM';
    const descendantsBeforeTerm = await collectDescendantIdentities(pid);
    // Signal leaves first, then the root.

View on GitHub (pinned to 8bc631a71a)

Solutions

  1. Re-run the stop command from the same (or elevated) context that spawned the process
  2. Catch ProcessTreeKillError and fall back to process.kill(pid,'SIGKILL') on the root, then verify the PID is gone
  3. Check the stderr in the message: 'Access is denied' means run elevated or fix ownership; exit code for timeout means retry or kill manually via Task Manager
  4. On POSIX this branch never fires; verify you are actually on Windows and the PID exists with tasklist before escalating

Example fix

// before
await killProcessTree(pid);
// after
try {
  await killProcessTree(pid);
} catch (e) {
  if (e instanceof ProcessTreeKillError) {
    logger.error(`tree kill failed for ${e.pid}; process may still be running`, { cause: e });
    // surface to CLI: do not report stop success
    process.exitCode = 1;
  }
}
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-check on Windows before killing
const { stdout } = await execFileAsync('tasklist', ['/FI', `PID eq ${pid}`]);
if (!stdout.includes(String(pid))) return; // already gone — skip kill

Type guard

function isProcessTreeKillError(e: unknown): e is ProcessTreeKillError {
  return e instanceof ProcessTreeKillError || (e instanceof Error && e.name === 'ProcessTreeKillError');
}

Try / catch

try {
  await killProcessTree(pid);
} catch (e) {
  if (isProcessTreeKillError(e)) {
    // do NOT report success; inspect e.message/e.cause for 'Access is denied' vs timeout
    throw new Error(`Failed to stop PID ${e.pid}; tree may still be running`, { cause: e });
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling killProcessTree(pid) on win32 when taskkill exits with a non-128 code — the target process is owned by another user/elevated context (Access denied), taskkill hits the 5s exec timeout, or the /T tree walk fails on a deep/wedged spawn chain.

Common situations: Stopping a worker/chroma subprocess spawned by an elevated shell from a non-elevated one; antivirus or policy blocking taskkill; a zombie process whose tree walk hangs past the timeout; running `server stop` while the child was spawned with different credentials.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of thedotmack/claude-mem@8bc631a71a (2026-09-09). Data as JSON: /api/errors/dafbc1b73b08acc2. Report an issue: GitHub.