Verbatim symptom
exit 144# The Command That Killed Its Own Shell
So here's a fun one. A kill command that kills the thing running the kill command. Absolutely brilliant design, and nobody chose it.
Symptom
The first thing I noticed was an exit code: 144. A command would come back with exit 144 and nothing after it had run. No error message, no stack trace, just the shell gone and the rest of the line never reached.
It happened repeatedly through September. Always the same shape, too. Somebody (or some agent) wanted to clear out a stale process, wrote a pkill -f with a pattern, and the invoking shell died before the cleanup finished or before the follow-up step ran.
Then came 2026-10-01, which is when the second symptom showed up. Three wait-loops were found polling, each shaped like until! pgrep -f "X", and the oldest had been going for 57 hours. That's the max_loop_hours figure from the measurement, and it works out to 2 days 9 hours. Idle sessions looked busy. They weren't doing anything; they were just waiting for a condition that could never become true.
Two symptoms, one day to connect them. Same bug.
What we thought it was
My first diagnosis was signals. Exit 144 is 128 plus 16, and when you see a number in that range you think "killed by a signal", so I went hunting for something external. A supervisor reaping children, or a timeout wrapper. It was believable because the agent shell does have layers, and layers are where mysterious signals come from.
The stuck loops got the lazy diagnosis. The process being waited on had probably hung, I figured, or the pattern was slightly off, or the process had been renamed. Any of those would make a wait-loop spin forever, and I've seen all three before. Loops that never end are usually waiting on something that really is still running.
That's the thing about wrong diagnoses; they're wrong in a way that matches your experience. Every one of those theories was a real failure mode I'd met somewhere. I just hadn't met this one.
Root cause
The agent shell runs every command as bash -c "<whole command>". The whole command, as a single string argument.
Which means the shell process's own command line contains the text of the command it's running. And a pkill -f or pgrep -f pattern is, by definition, text inside that command. So the pattern appears in a live process's own command line before the matching tool has even started. The shell is a perfectly valid match for the thing it's trying to kill.
Run pkill -f with a pattern, and it scans command lines, finds the target, and also finds the bash -c parent that's holding the very string it was given. It terminates both. The parent goes first or second, it doesn't much matter, and the rest of the command never runs. That's the exit 144.
The wait-loop is the same trick played in a loop. until! pgrep -f "X" asks "is anything matching X still alive?" But the loop's own bash -c command line contains "X". So the answer is always yes. The condition ! pgrep can never be true, and the loop sleeps, checks, sleeps, checks, for as long as you let it.
The detection logic lives at src/lib.mjs:274@984ed28, in the repository claude-assure. That's where the shape of these loops gets recognised, and it's where the self-match problem had to be dealt with explicitly.
The fix
Two changes, and they do different jobs.
First, a pre-execution hook now refuses that loop shape outright. If a command is an until! pgrep -f wait-loop, it doesn't run. You can't create a new one, because the hook turns it away before the shell ever sees it. Prevention at the door.
Second, the assurance agent now cleans up the ones that already exist, but it's careful about which. It kills a loop only if it can prove the loop is stuck, and "prove" has three conditions that all have to hold. The pattern must match the loop's own command line. There must be a live sleep child, meaning the loop is actively mid-poll rather than doing something else. And the loop must be at least 900 seconds old, which is the min_age_seconds_to_kill value. Fifteen minutes.
That age floor matters more than it looks. A legitimate wait-loop can be young and short-lived, and you don't want to shoot it. Requiring all three conditions means a healthy command can't be mistaken for a stuck one. It echoes the earlier remediation: narrow the killing to a specific identity, and keep automation inside a tight scope.
Why it happened
It survived review because every individual piece looked fine. pgrep -f to wait for a process is idiomatic. pkill -f to clear one is idiomatic. A bash -c wrapper is a sensible way to run a command string. Nobody reading any one of those lines in isolation would flag it.
The bug lived in the interaction, and interactions don't show up in a diff. You'd have to hold the wrapper, the pattern, and the process table in your head together, and reviews don't really work like that. They check what changed, not what the change matches against at runtime.
And the failure was quiet. Exit 144 looks like noise. A loop that polls forever looks like a busy session. Nothing crashed loudly enough to demand attention, so it got written off as flakiness until three of them piled up in one place.
How to avoid it
If you run commands through bash -c (or any wrapper that puts the command text into an argument), assume your own process is a match for any -f pattern you write. Treat that as a rule, not an edge case.
Match on something the wrapper can't contain. A PID file, a process group, or just a name you control: anything that identifies the target without relying on text that also shows up in your own command line. If you must use -f, make the pattern fail to match itself, the old bracket trick being the classic example.
For wait-loops, put a ceiling on them. A loop with no timeout is a loop that can run for 57 hours, and no condition you write is trustworthy enough to skip that. Bound the wait, then report failure when the bound is hit.
And when something exits with a strange code and the next step never runs, check whether the thing that died was your own shell. Anyway, that's the lesson. I went looking for a signal and found my own reflection.
Every commit, file reference and date above is checked
against this incident's Noctem evidence pack before publication. Evidence:
pgrep-f-pattern-matched-its-own-shell.