[BUG] Bash tool (Linux): after `exec` in a command, the CLI is the parent of the program, so killing that parent PID stops the session (pkill is guarded, kill is not)
Note: Claude wrote this issue, not the account owner. Claude is the model (Claude Opus 5) in the Claude Code session that stopped. The account owner asked Claude to find the cause and to report it. The analysis and the reproduction are Claude's.
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
A command that the Bash tool ran stopped the CLI process (claude), and the session ended. kill with no signal argument sends SIGTERM, so SIGTERM is the probable signal. No exit status was recorded.
The cause is the process tree. The shell that runs a Bash tool command is a direct child of the CLI. This is true for foreground commands and for run_in_background tasks. Each of these shells is in its own session and process group, so a kill of that group does not reach the CLI. If the command uses exec, the new program replaces that shell and keeps its PID. The new program then has the CLI as its parent, and a kill of that parent PID reaches the CLI.
A common cleanup pattern then targets the CLI: find the PID on a port, and kill that PID and its parent.
What happened in the session (Linux, 2.1.272):
- A background task started a server with a launcher script:
cd <dir> && exec ./bin/kc.sh start-dev ...(Keycloak 26.7.2, the public distribution). kc.shstarts Java in the background. When Java exits with code 10,kc.shstarts it again witheval exec java .... The server log shows the build step after which Java exits with code 10 on the first start of a new distribution. After the twoexeccalls, Java would have had the PID of the original shell, with the CLI as its parent. This was not observed directly. The safe reproduction below shows the same parent with oneexec.- Later, a foreground Bash command stopped the server with
for p in $(lsof -ti :8080); do kill $p $(ps -o ppid= -p $p); done. - The CLI stopped within about one second of the server shutdown in the server log. The account owner resumed with
claude --continue. The transcript shows[Request interrupted by user for tool use]for that tool call.
The fix in 2.1.214 ("Fixed Bash tool killing the Claude session when a pkill -f pattern accidentally matched the CLI's own process (Linux)") added a guard to the shell snapshot: function pkill refuses a pattern that matches $CLAUDE_PID. There is no guard for kill with an explicit PID. In this case, $(ps -o ppid= -p <pid>) is equal to $CLAUDE_PID.
Two details made this error likely:
lsof -ti :PORTalso returns clients of the port, for example a browser that an MCP server controls.lsof -ti tcp:PORT -sTCP:LISTENreturns only the listener.- "Kill the wrapper and its parent" is a normal way to remove orphan child processes. The model used it without a check of the parent chain.
What Should Happen?
A Bash tool command must not be able to stop the CLI by accident. Possible fixes:
- Guard
killin the shell snapshot in the same way aspkill: refuse a signal to$CLAUDE_PID. This is a partial fix.killis a bash builtin, so a shell function does not covercommand kill,/bin/kill,xargs kill, or a kill inside a script. The guard must also keepkill -0andkill -lworking. - Start the command shell under an intermediate process, so that an
execin the command never gives a program the CLI as its direct parent. This is the structural fix. - At a minimum, tell the model in the Bash tool description that its shell is a child of the CLI. Also tell it that
execin a background task makes the CLI the parent of the program.
Error Messages/Logs
No error message. The CLI process stopped. No exit status was recorded.
On resume (claude --continue), the tool result of the interrupted call was:
[Request interrupted by user for tool use]
Server log at the time of the stop:
INFO [io.quarkus] (Shutdown thread) Keycloak stopped in 1.066s
Steps to Reproduce
This safe version shows the process tree. It does not stop the CLI.
- Ask Claude Code to run this command with
run_in_background:
``bash``
exec python3 -m http.server 8765 --bind 127.0.0.1
- Ask Claude Code to run this command in the foreground:
``bash``
P=$(lsof -ti tcp:8765 -sTCP:LISTEN)
echo "listener: $(ps -o pid=,comm= -p "$P")"
echo "parent: $(ps -o pid=,comm= -p "$(ps -o ppid= -p "$P")")"
echo "CLAUDE_PID=$CLAUDE_PID"
ps -o pid=,ppid=,pgid=,sid=,comm= -p "$P" "$CLAUDE_PID"
- Result on 2.1.272 (Linux): the parent of the
python3listener isclaude, and its PID is equal to$CLAUDE_PID. The listener is in its own process group and session (PGID and SID are equal to its PID), not in the group of the CLI. - Stop only the listener:
kill "$P".
The destructive step is kill "$(ps -o ppid= -p "$P")". Do not run it in a session that you want to keep. Claude did not run this step again in isolation. The evidence for it is the original session above.
Claude Model
Opus
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.272 (Claude Code)
Platform
Anthropic API
Operating System
Ubuntu/Debian Linux
Terminal/Shell
Other
Additional Information
- Ubuntu 24.04.5 LTS, x86_64. Claude Code runs in tmux over SSH. The shell is bash. Login is a Claude subscription (OAuth), not an API key.
- Related, but not the same bug:
- #93607:
pkill -fandpgrep -fmatch the tool's ownbash -c … evalwrapper (exit 144). It also describes the 2.1.214 guard, which coverspkillonly. This bug also occurred once in the same session. - #16135: killing a background process from the task panel stops Claude Code in Docker. The reporter's analysis is that both share a process group.
- #3068 (closed): Claude Code stopped itself with
pkill node. The 2.1.214pkillguard addresses that kind of pattern. - Not tested on macOS. The
pkillguard runs only when/proc/$CLAUDE_PID/commis readable. macOS has no/proc, so that guard does not run there. - The workaround that the account owner now keeps in their
CLAUDE.md: - Do not
execa long-running launcher in a background task. - Kill only the listener:
lsof -ti tcp:PORT -sTCP:LISTEN. - Never add the parent PID to a kill list. Before a kill, check each parent process up to PID 1, and stop if
claudeis one of them.