OS & PROCESS TRIAGE
Killing Ghost Terminals: How to Stop AI Agents from Freezing on Port Conflicts and Interactive Prompts
The engineering behind silent subshell hangs: pipe deadlocks on unhandled (y/n)? prompts, rogue test watchers, and orphaned worker threads locking ports :3000 and :5173.
The Silent Freeze: The Developer's Worst Nightmare
When an autonomous AI agent (Cursor, Claude Code, Cline) runs a command in a background subshell, things frequently go dark. You stare at a spinning indicator for 10 minutes. The agent outputs nothing. Your CPU fan spins at maximum RPM.
When you finally inspect the process list using ps aux | grep node, you find a graveyard of 15 orphaned worker processes. Port 3000 is locked. Port 5173 is occupied. Your Git repository has a stale .git/index.lock.
The 3 Silent Hang Vectors in Agent Execution
Autonomous agents execute commands through non-interactive POSIX child processes with redirected pipes (stdin, stdout, stderr). This creates three deadly traps:
1. The Interactive Prompt Trap
Many CLI utilities (such as prisma migrate, npm init, or gcloud) attempt to detect if they are running in an interactive TTY. If that detection is imperfect, the tool pauses and prints: Are you sure you want to proceed? (y/N):. Because the agent waits for stdout to close, both the process and the agent freeze in an unrecoverable pipe deadlock.
2. The Rogue Watcher Loop
Modern test runners and bundlers (vitest, jest, vite) default to interactive watch mode unless explicitly passed --run or --watch=false. If an agent issues pnpm test, Vitest enters watch mode and waits forever for file changes. The agent waits for an exit code that will never arrive.
3. Orphaned Worker Zombies and Port Trapping
When an agent IDE eventually times out and kills the top-level command, it typically sends SIGINT only to the parent PID. The worker subprocesses (esbuild, vitest-worker) are re-parented to PID 1 (init/systemd) and continue running, holding open socket listeners on :3000 or :5173. The next command crashes with EADDRINUSE.
When an AI agent sees EADDRINUSE :::3000, it lacks OS awareness. It assumes the port configuration is broken, modifies vite.config.ts to use port 3001, then 3002, churning configuration files while the real zombie process keeps running silently in the background.
The Solution: Active Terminal Guardian (pray-run)
To eliminate this systemic fragility, ctrl-alt-pray provides the Active Terminal Guardian wrapper:
# Wrap any test or build command:
pray-run pnpm test
pray-run npm run dev
1. The 15-Second Silence Watchdog
Unlike blunt execution timeouts that kill legitimate 5-minute compilations, pray-run monitors stream silence. As long as output flows on stdout/stderr, the timer resets. The instant output stream goes completely silent for >15 seconds, the Guardian intervenes.
2. The Cascade Process-Tree Killer
Standard process.kill(pid) leaves worker sub-processes stranded. The Guardian performs recursive process tree traversal:
// Recursive PID discovery across child processes
const getChildPids = (parentPid: number): number[] => {
try {
const stdout = execSync(`pgrep -P ${parentPid}`, { encoding: "utf8" });
const pids = stdout.trim().split("\n").map(Number).filter(Boolean);
return [...pids, ...pids.flatMap(getChildPids)];
} catch {
return [];
}
};
When triggered, pray-run:
- Sends
SIGTERMto the entire PID tree simultaneously. - Waits a 1,500ms grace period for graceful teardown.
- Escalates to
SIGKILL (-9)for surviving worker processes. - Frees occupied dev ports (
3000,4321,5173) and purges stale.git/index.lockfiles.
3. Flapping Exit Code Circuit Breaker
If the same failing exit code repeats 3 consecutive times, pray-run injects an unmissable terminal circuit breaker banner directly into the agent's context, commanding it to halt speculative edits and invoke the pray MCP tool.
Banish Terminal Zombies in Your Workspace
Zero external runtime dependencies. Built strictly on Node.js 22+ standard library.
npx ctrl-alt-pray init
Star on GitHub