[BUG] Windows: removing CLAUDE_CONFIG_DIR while claude.exe is running pins one CPU core indefinitely (no error, no backoff)
Summary
If the directory named by CLAUDE_CONFIG_DIR is removed while an interactiveclaude.exe session is running, the process goes from ~0 to ~1.1 cores and
stays there. Nothing is logged or shown, memory stays flat, and it never
recovers. Deleting only the directory's contents does not trigger it. Removing
the directory itself does.
This turns up in practice when a terminal host gives each pane a throwaway
config dir and deletes it when the pane closes. If the claude.exe in that pane
outlives the close (see "Second, related problem" below), it ends up pinning a
core for days.
Environment
- Claude Code 2.1.292 (
claude --version->2.1.292 (Claude Code)), nativeclaude.exe - Windows 11 Pro 10.0.26200, ARM64
- Interactive session in a ConPTY (node-pty), idle at the first screen, no prompt sent
CLAUDE_CONFIG_DIRset to a fresh empty temp dir on NTFS (C:)
Steps to reproduce
- Create an empty temp directory, e.g.
%TEMP%\cfg-test. - Start
claude.exein a ConPTY withCLAUDE_CONFIG_DIR=%TEMP%\cfg-test.
Leave it idle for ~25 s so it starts up and writes its files into the dir (4 entries in our run).
- Measure CPU for 30 s -> ~0.
- Delete everything inside the directory but keep the directory. Measure 30 s -> still ~0.
- Remove the directory itself. Measure 30 s -> ~1.1 cores, sustained.
Measured result (CPU-seconds delta of the claude.exe pid over 30 s each):
| phase | state of CLAUDE_CONFIG_DIR | avg cores |
|---|---|---|
| A | present, populated | 0.002 |
| B | present, emptied | 0.001 |
| C | directory removed | 1.109 |
In the field, survivors in this state ran for 15-42 h at a steady 0.7-1.0 core
with flat memory before they were killed by hand.
Minimal repro script (Node 22 + node-pty). It is the same logic as the run above:
const path = require('path'), fs = require('fs'), os = require('os');
const { execFileSync } = require('child_process');
const pty = require('node-pty');
const CLAUDE = process.argv[2]; // path to claude.exe
const cfg = fs.mkdtempSync(path.join(os.tmpdir(), 'cfg-'));
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const cpu = (pid) => Number(execFileSync('powershell', ['-NoProfile', '-Command',
`(Get-Process -Id ${pid}).CPU`], { encoding: 'utf8' }).trim());
async function measure(pid, label) {
const a = cpu(pid); await sleep(30000);
console.log(label, ((cpu(pid) - a) / 30).toFixed(3), 'cores');
}
(async () => {
const p = pty.spawn(CLAUDE, [], { cols: 100, rows: 30, cwd: os.tmpdir(),
env: { ...process.env, CLAUDE_CONFIG_DIR: cfg } });
p.onData((d) => { if (d.includes('\x1b[6n')) p.write('\x1b[40;1R'); });
await sleep(25000);
await measure(p.pid, 'A dir present');
for (const e of fs.readdirSync(cfg)) fs.rmSync(path.join(cfg, e), { recursive: true, force: true });
await sleep(5000); await measure(p.pid, 'B contents deleted');
fs.rmSync(cfg, { recursive: true, force: true });
await sleep(5000); await measure(p.pid, 'C dir removed');
execFileSync('taskkill', ['/F', '/T', '/PID', String(p.pid)]);
})();
What should happen
- When a watched root (the config dir, or any directory a watcher is attached to)
disappears, the watcher should stop or back off: poll for it to come back at a
bounded rate, or give up after N attempts and log one warning. It should not
retry in a tight loop.
- A session whose config dir is gone cannot persist anything anyway. Showing a
one-line warning, or exiting, would be better than spinning silently.
What happens
- One core, sustained, forever. No error on screen, nothing in the transcript, flat memory.
- I did not capture a stack or I/O counters for this exact run. The shape (one
core, idle, flat memory, Windows, directory gone) matches the watcher/lookup
loops in #76608, #81222 and #100831. The likeliest cause is the bundled
chokidar add() re-adding a path or its parent with no backoff once the
directory is gone, but I have not verified that.
Win32_Process.OtherOperationCount deltas or a Process Monitor trace on the
spinning pid would confirm it.
Impact
A terminal host that gives each pane its own CLAUDE_CONFIG_DIR and cleans it up
when the pane closes leaves behind a claude.exe that pins a core until someone
notices, if the process survived the pane close. Several of these piled up here
over two days, each at ~1 core. The user sees nothing: the pane is gone and the
process has no window.
Second, related problem: claude.exe can outlive its console
The spinning processes above were only possible because claude.exe kept running
after its pane closed. In our case the host typed an exit sequence
(Ctrl+C, then exit) into the pane before closing it. Sessions that had a
dialog open (for example the rate-limit options screen), or were in the middle of
a tool call or a background task, did not act on it. When the host then closed the
ConPTY, claude.exe did not exit. The "survives console close" part comes from
field observation and was not isolated in a controlled repro. Expected behaviour:
when the console/ConPTY goes away (the console handle becomes invalid,
CTRL_CLOSE_EVENT, or stdin EOF on the pty), an interactive session should shut
down the way it does on SIGTERM. At minimum it should stop all polling. This may
belong in a separate issue. It is the Windows counterpart of #90032.
Related issues (none is an exact duplicate)
- #81222 (open, Windows): a session cwd bound to an empty/deleted worktree spins at 1.00 core
doing a no-backoff .claude lookup. Same class. There the cwd is missing; here it is
CLAUDE_CONFIG_DIR, and the cwd is intact.
- #100831 (open, Windows): bundled chokidar re-adds
C:/in a loop when realpath fails
(AppContainer). Probably the same no-backoff add() path, but it is triggered by realpath
failing everywhere, not by a directory that vanishes mid-session.
- #76608 (closed stale, Windows): settings watcher loops on
lstatEISDIR on an ImDisk RAM disk,
~60k stat/s. Same "persistent watcher error, no backoff" shape, but the trigger is the filesystem.
- #100167 (open, Windows): hooks disappear after the project volume dismounts and remounts.
Same "watched path vanishes mid-session" trigger, but the symptom is lost hooks, not CPU.
- #90032 (open, feature, macOS): handle SIGHUP for a graceful exit when the terminal closes.
It covers the "outlives its terminal" half, but for macOS/SIGHUP and only the graceful-exit
message. It does not cover Windows console close or leftover CPU use.
Workaround for terminal hosts
- Do not remove a session's
CLAUDE_CONFIG_DIRuntil thatclaude.exeis confirmed dead.
Deleting the dir's contents is harmless (phase B). Removing the dir itself starts the spin.
- On pane close, find the actual
claude.exepid (for example by walking parent pids from the
pane's shell) and kill it after a short grace period. Killing the pty's root process is not
enough if that root is a wrapper that has already exited.
- Send
Escbefore any typed exit sequence, so an open dialog does not swallow it.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗