跳到正文
DEV Community· SynuraCore·· 2 小時前AI 評分63

Claude Code 的 shell 為何會被 pkill -f 終止,以及修復方法

Claude Code killed its own shell with pkill -f (exit 144): why, and a 2-minute fix

AI 導讀

Claude Code 執行 pkill -f server.js 時,該模式可能匹配包含整條指令的 Bash shell,令 shell 和工具呼叫以 exit 144(有時 143)結束。

正文

You ask Claude Code to restart a dev server. It runs:

pkill -f server.js && node server.js &

The tool call ends with exit 144 (sometimes 143). Nothing after pkill runs. The server is still the old one, or there is no server at all. If the agent used a wait loop instead, while pgrep -f server.js; do sleep 1; done never ends.

This is anthropics/claude-code#93607. We wrote it down as a rule, then hit it several more times in two days, so we stopped it mechanically. Here is what happens and how to fix it.

Why it happens

pkill -f matches the pattern against the full command line of every process, not just the program name.

Claude Code's Bash tool runs your command inside a shell, and that shell's own command line contains your whole command: the tool's wrapper plus pkill -f server.js && node server.js &. So the string server.js is in the shell's command line. pkill finds a match, and the match is the shell that is running pkill. The shell dies, and so does the tool call.

In a normal terminal you rarely see this, because your interactive shell's command line is just bash. In an agent, every command is wrapped, so the pattern is always present.

Three fixes

1. pkill -A -f (best, if you have it). -A tells pkill to ignore its own ancestors, so the shell running it is skipped. It needs procps-ng 4.0.3 or newer (check with pkill --version). On Claude Code 2.1.316 on Linux, pkill -A -f server.js killed the server and the shell survived.

pkill -A -f server.js

2. The bracket trick, with a catch. pkill -f '[s]erver.js' works because the regex [s]erver.js matches the text server.js but not the text [s]erver.js. The catch, pointed out in the issue thread: if the plain word appears anywhere else in the command, for example cd ~/server.js-app && pkill -f '[s]erver.js', the shell matches again and dies.

3. A PID file. Write the PID when you start the server (node server.js & echo $! > .server.pid) and stop it with kill "$(cat .server.pid)". No pattern, no self-match. This is also the fix for BusyBox and older procps, which have no -A.

Making the agent stop doing it

Telling Claude "don't use pkill -f" in CLAUDE.md did not hold. The pattern came back during debugging, when the agent was focused on something else. A rule the agent has to remember is a rule it will eventually forget.

So we turned it into a PreToolUse hook: before a Bash command runs, check whether a pkill -f / pidwait -f pattern (or a pgrep -f whose result is used) would match the command's own text, and deny it with the fix if -A is missing. Claude reads the reason and rewrites the command itself.

It is packaged as a plugin. Two lines in Claude Code:

/plugin marketplace add hadiux0295/claude-code-mistake-guards
/plugin install synura-mistake-guards@synuracore

What it does and does not do:

  • One PreToolUse hook on Bash, about 100 lines of standard-library Python. It reads the pending command from stdin and prints a deny decision when the pattern would hit its own shell.
  • No network calls, no files written, nothing else read. Needs Python 3.
  • It passes pkill -A -f, bracket patterns that really don't self-match, and plain mentions of pkill in text.
  • It cannot check pkill -f "$PATTERN": the value is unknown before the shell runs.
  • Override one command on purpose with GUARD_OK=self_kill in it.

We tested it on our own session logs before shipping: of 121 real pkill -f calls, it would have denied 61. The other 60 were bracketed patterns that really don't self-match, or the word appearing only inside text. The plugin ships with 37 tests of its own (test_plugin.sh), all passing.

Credit: the -A fix and the bracket edge case come from gweber and corneliusroemer in the #93607 thread. The plugin only packages that check, with tests.

The bigger pattern

This bug is a good example of something we kept seeing in five months of daily agent work: the mistakes that repeat are the ones that look like success. Exit 144 looks like a flaky command. A wait loop that never ends looks like a slow server. The agent moves on.

The repo has six more guards like this one (bare git pull in a shared tree, saves that overwrite a record with a placeholder, replies that read bot traffic as demand, and more), each with the incident it came from: github.com/hadiux0295/claude-code-mistake-guards. There are also 50 of the mistakes in English, each with a one-line CLAUDE.md rule.

Who wrote this: the incident records were written by the AI that made the mistakes (Claude Code), as it worked. This post and the tests were written by AI (Claude) and checked against the session logs and the issue thread. A human set the rules and runs the setup.

來源:DEV Community · dev.to