Claude Code won't exit? Kill it from the process tree!
One of my long-term goals using Claude Code is to create a truly “autonomous” development workflow while I’m building UnleashedPodcasts.com. I’ve been using a “Ralph Wiggins” loop as the basis, and I asked Claude Code to run “/exit” when it’s completed with the work, but both of us would just wait and wait forever. Everything was done, but nothing was moving forward.
I’d fallen into a serious problem with my plans.
What Ralph Loops Are (and Why They Break)
A Ralph loop is an automated Claude Code session where Claude runs through a defined set of work and then exits so a new session can start fresh. The idea is that you kick off a loop, Claude does a chunk of work, terminates cleanly, and then the loop script restarts a new session to pick up where the last one left off. It’s a way to work around context limits and keep long-running AI-assisted workflows moving without babysitting them.
The “exit” part is supposed to be simple. You tell Claude: when you’re done with the task, run /exit to end the session.
Except /exit isn’t a command that Claude runs, and inside an automated session with no human watching the terminal, that might go nowhere. The session stays open, and the loop stalls, so I have to manually kick off /exit.
I tried a ton of variations. I told Claude over and over to force the use of /exit, rephrasing the instruction until I couldn’t think of anything else to try. I even added, “and I mean ACTUALLY exit” to the prompt, which did not help.
The Insight: Claude’s Bash Scripts Are Children of Claude
Here’s what finally clicked. LLMs like Claude are good at non-deterministic tasks like “planning” or “writing,” but they’re not always the best at doing deterministic tasks like “exit”.
However, when Claude runs a bash script, that script runs as a child process of the Claude process. Which means from inside the script, you can look up the process tree, find Claude sitting there as a parent, and kill it directly.
Instead of telling Claude to exit (which requires Claude to cooperate), you write a script that finds Claude in the OS process tree and terminates it with a kill -9. Claude doesn’t have to do anything. The process just stops.
The script lives as part of my “support-scripts” in .claude/support-scripts/kill-claude.sh. Here’s the full thing as of May 8th, 2026 (check back for updates if I run into an issue).
#!/bin/bash
# Script to kill the Claude process by traversing up the process tree
# This allows Claude to terminate its own session when instructed to "exit"
set -e
# Start with the current process
current_pid=$$
echo "Starting from PID: $current_pid"
# Function to get parent PID
get_parent_pid() {
local pid=$1
ps -o ppid= -p "$pid" | tr -d ' '
}
# Function to get process command
get_process_command() {
local pid=$1
ps -o comm= -p "$pid" 2>/dev/null || echo ""
}
# Traverse up the process tree to find "claude"
max_iterations=20
iteration=0
while [ $iteration -lt $max_iterations ]; do
iteration=$((iteration + 1))
# Get the command for current PID
command=$(get_process_command "$current_pid")
echo "PID $current_pid: $command"
# Check if this is the claude process
if [[ "$command" == *"claude"* ]]; then
echo "Found Claude process at PID: $current_pid"
echo "Killing Claude process..."
kill -9 "$current_pid"
echo "Claude process terminated."
exit 0
fi
# Get parent PID
parent_pid=$(get_parent_pid "$current_pid")
# Stop if we've reached init (PID 1) or can't get parent
if [ -z "$parent_pid" ] || [ "$parent_pid" -eq 1 ]; then
echo "Reached top of process tree without finding Claude process."
exit 1
fi
current_pid=$parent_pid
done
echo "Max iterations reached without finding Claude process."
exit 1
The $$ variable is the PID of the currently running bash script. From there, the loop calls ps -o ppid= to get the parent PID of whatever process we’re currently looking at, then checks whether that process’s name contains “claude”. If it does, we kill it. If we reach PID 1 (the system init process), we give up and exit with an error.
In practice, Claude is usually only two or three levels up the tree. The loop hits it quickly, issues the kill -9, and the session terminates hard. No waiting for Claude to cooperate. No hoping. Just gone.
How to Wire It Into Your Loop
The script needs to be executable. After you create it at .claude/support-scripts/kill-claude.sh, run:
chmod +x .claude/support-scripts/kill-claude.sh
Then, in your Claude Code custom command or loop instructions, tell Claude to call the script when it’s ready to exit:
When you have completed all tasks, run `.claude/support-scripts/kill-claude.sh` to end the session.
Do not use /exit. Call the script.
Being explicit about “do not use /exit” matters. You want to override that instinct with a specific instruction pointing at the script.
If you’re using a shell loop to restart sessions, you’d chain it something like this (with your actual loop logic replacing the placeholders):
while true; do
cat ralph-prompt.md | claude --permission-mode acceptEdits
echo "Session ended. Starting next session..."
sleep 2
done
Claude calls kill-claude.sh, the session terminates, the shell loop detects that the process exited, pauses briefly, and starts a new Claude session. Clean, automatic, and it actually works.
A Few Things to Keep in Mind
The process name matching uses a simple *claude* glob, which has worked reliably in every environment I’ve run it in. If, for some reason, you’re running a different setup where the process name is different, you’d adjust that check.
The script does exit with code 1 if it can’t find Claude, which means you can check the exit code in your loop script if you want to handle that case differently.
And yes, this script lives at .claude/support-scripts/kill-claude.sh in my actual project, and it’s called from my actual loop commands. That part isn’t hypothetical. This is the thing that finally made Ralph Loops reliable for me.
The whole approach is a little inelegant, I’ll admit. “Kill the process from inside” isn’t exactly the graceful exit I was hoping to build. But it’s predictable, it’s scriptable, and it works every single time. In an automated workflow, predictable beats elegant.


