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
- Re-run the stop command from the same (or elevated) context that spawned the process
- Catch ProcessTreeKillError and fall back to process.kill(pid,'SIGKILL') on the root, then verify the PID is gone
- 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
- 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
- Spawn children from the same elevation/user context that will stop them
- Keep spawn chains shallow on Windows to reduce wedged /T walks
- Always catch ProcessTreeKillError in stop paths and propagate failure to the CLI exit code
- Check taskkill stderr in the message to distinguish access-denied from timeout
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
- tree-kill failed for PID ${pid}: ${error instanceof Error ?
- Failed to locate codex via where; falling back to codex.cmd
- Worker unavailable on Windows — skipping spawn (recent attem
- Connection failed, killing subprocess tree to prevent zombie
- chroma-mcp uvx prewarm failed
AI-assisted analysis of thedotmack/claude-mem@8bc631a71a (2026-09-09).
Data as JSON: /api/errors/dafbc1b73b08acc2.
Report an issue: GitHub.