ctrl-alt-pray
Back to Articles

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.

The Compounding Port Hallucination

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:

  1. Sends SIGTERM to the entire PID tree simultaneously.
  2. Waits a 1,500ms grace period for graceful teardown.
  3. Escalates to SIGKILL (-9) for surviving worker processes.
  4. Frees occupied dev ports (3000, 4321, 5173) and purges stale .git/index.lock files.

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