---
title: "Surviving Trailing Prompts: The 10-Minute Grace Window"
canonical: https://dxdev.com/blog/2026-09-15_graceful-session-close-zombie-prevention/
datePublished: 2026-09-15
---
The session was fresh at 4:31 PM. By 6:06 PM it was closed: server off the LAN, metered key out of the environment, and one line I'd been putting off finally verified: "closed sessions no longer zombie back to life from a trailing prompt."

Here's the actual bug. Every Claude Code session writes a manifest file in the vault, one YAML block per session, `status: active` or `status: closed`. A hook fires on every `UserPromptSubmit` and reads that manifest to render the footer and route `/next`, `/sync`, `/close`. The assumption baked into the original code was that a prompt only ever arrives while a session is alive. Wrong. A prompt can arrive after `/close` too, because closing a Claude Code session doesn't atomically kill the process that's still capturing your keystrokes. You type something into a pane you thought was dead, or a queued message lands a few seconds late, and the hook sees `session_id` match a manifest, sees a prompt event, and does the only sensible thing it knows how to do with a prompt event: flips `status` back to `active`, clears `ended`, appends a new sub-session row. The session you closed five minutes ago is alive again, its footer rendering, its ticket state drifting from whatever you actually did next.

I first tried suppressing this by having `/close` kill the hook's write access for a fixed window, just delete the manifest's ability to be touched for 60 seconds after `ended` gets set. That's the wrong shape of fix and I found out why the hard way: 60 seconds isn't a grace window, it's a race. I ran it against a real close and then genuinely walked away from the keyboard for a minute to grab coffee, and the trailing prompt landed at 63 seconds. Zombie, right through the block. Tightening the window doesn't fix a category error, it just moves the coin flip. A hard lockout also breaks the actually legitimate case: someone closes at 5:58 and genuinely comes back at 6:02 to keep working. That's not a zombie, that's a resume, and a blanket lock can't tell the difference from a stray prompt.

The fix that shipped instead reframes the question. It's not "how long do we block writes," it's "how do we classify the first prompt after close." So `cmd_prompt()` in the session hook checks the manifest's `ended` timestamp against now. Under 10 minutes: this is the grace window, and the very first prompt in it gets special treatment, the session stays `status: closed`, its `outcome` and `duration` untouched, but the hook stamps a `post_close_prompt` field onto the manifest instead of reopening it. Past 10 minutes: no such thing as a stray prompt anymore, this is a deliberate resume, so `status` flips to `active`, `ended` clears, and a fresh sub-session row gets appended, exactly the old zombie behavior, but now it's correct behavior because at 25 minutes out nobody's finger just slipped.

I wrote it as a script instead of trusting my read of the diff, built a fake closed manifest with `last_heartbeat` 2 minutes before "now," fired the prompt hook against it twice. First prompt: status stayed `closed`, duration held at `44m`, `post_close_prompt` got set to `True`. Second prompt on the same dead session: status flipped to `active`, `ended` cleared, a new sub-session row appeared. Then the edge case that actually matters, a manifest closed 25 minutes ago getting a prompt: status went straight to `active` on the very first prompt, no grace, because 25 minutes past `ended` a prompt has no business pretending to be an accident.

The 10-minute number isn't tuned to anything scientific, it's just longer than the round-trip of "I closed the pane, then my finger caught the enter key while the terminal was still capturing," and shorter than any plausible "I stepped away and I'm back." The mechanism is what makes it hold: not a lock, a classifier that looks at one timestamp and treats the first prompt in the window as noise, everything after it as intent.
