Skip to content
← All notes

// notes

pkill -f kills itself: the pattern matches the command line

pkill -f and pgrep -f match the pattern against the whole command line, including the bash -c wrapper. Why the command kills itself and which filters help

Published: 2026-10-176 min read
Linuxbashpkillpgrepdebugging

A script breaks off mid-sentence with [killed by signal: SIGTERM], even though there is no kill of its own PID anywhere in it. pgrep -f matches a pattern against the whole command line of a process, not against the program name, and that line includes the text of the bash -c wrapper the script runs inside

Below: how to diagnose it, why it happens, and three replacements for -f

Symptom

The command finishes without a traceback: the output stops halfway and only the signal marker is left in the log

$ ./restart-agent.sh
stopping the old process
[killed by signal: SIGTERM]
$ echo $?
143

Code 143 is 128 + 15, that is SIGTERM. The OOM killer does not fit here: it sends SIGKILL and code 137. So the signal came from one of the script's own commands

A telling detail: run by hand in an interactive shell, the script works; inside a bash -c wrapper it dies. That is how CI runners, tool shells and build systems start a command: the whole script arrives as a single argument

Diagnosis

Start with who the pattern actually selects. The -a flag makes pgrep print the PID and the full command line of every match

$ pgrep -a -f 'authentication-agent'
4242 bash -c PAT='authentication-agen[t]'; old=$(pgrep -f "$PAT"); ...
4301 /usr/lib/polkit-kde-authentication-agent-1

The list contains a bash -c wrapper with the same text as the pattern. That is the process that receives the signal. You can look at your own command line directly

$ echo $$
4242
$ tr '\0' ' ' < /proc/$$/cmdline; echo
bash -c PAT='authentication-agen[t]'; old=$(pgrep -f "$PAT"); ...

The file /proc/<pid>/cmdline stores arguments separated by a null byte, pgrep joins them with spaces and applies the pattern as an extended regular expression. With -f there is no separate check that the match landed on the program name

Cause

pgrep -f walks every process, reads /proc/<pid>/cmdline and tests the pattern against the whole argument string. For a process started as bash -c '<script>', that string contains the script in full: both the assignment PAT='authentication-agen[t]' and the path /usr/lib/polkit-kde-authentication-agent-1

The square-bracket trick is designed for one case only. The pattern authentication-agen[t] is a regular expression where [t] denotes a class of a single character: it matches the text authentication-agent but does not match the text authentication-agen[t]

As long as the pattern appears in the command line only by itself, there is no self-match. Here authentication-agent appears a second time, inside the path to the binary, and the brackets do not protect against that occurrence

$ printf '%s\n' "PAT='authentication-agen[t]'" "/usr/lib/polkit-kde-authentication-agent-1" \
    | grep -E 'authentication-agen[t]'
/usr/lib/polkit-kde-authentication-agent-1

The next trap is trying to exclude yourself by comparing against $$. In bash $$ is the PID of the shell that started, and it does not change in a subshell: inside ( ... ), in the pipeline cmd | while read and in a $( ) substitution the variable stays the same. The real PID of the current process comes from $BASHPID

$ echo "shell: $$, current: $BASHPID"
shell: 4242, current: 4242
$ ( echo "shell: $$, current: $BASHPID" )
shell: 4242, current: 4388

If the check [[ "$p" == "$$" ]] && continue runs in a subshell, it skips the parent but not the current process, and kill hits that one

Fix

It is safer to rely not on the argument text but on the path to the file that is actually executed. readlink /proc/<pid>/exe returns it, and without truncation

The loop drops its own PIDs and kills only a process whose exe points at the agent. Do a dry run first: echo stands in for kill

AGENT=/usr/lib/polkit-kde-authentication-agent-1
for p in $(pgrep -f 'authentication-agent' 2>/dev/null); do
  [[ "$p" == "$$" || "$p" == "$BASHPID" ]] && continue
  [[ "$(readlink /proc/$p/exe 2>/dev/null)" == "$AGENT" ]] && echo kill "$p"
done

Review the list first, then drop the echo. The production version adds an ownership check: the signal goes only to your own process

AGENT=/usr/lib/polkit-kde-authentication-agent-1
for p in $(pgrep -f 'authentication-agent' 2>/dev/null); do
  [[ "$p" == "$$" || "$p" == "$BASHPID" ]] && continue
  [[ "$(readlink /proc/$p/exe 2>/dev/null)" == "$AGENT" ]] || continue
  [[ "$(stat -c %u /proc/$p)" == "$(id -u)" ]] && kill "$p"
done

For another user readlink returns an empty string and stat reports a different UID, so such a PID fails the condition. That is a safe failure: better not to kill than to kill the wrong thing

If the program name is unique, pkill -x is enough. Without -f it matches the pattern against comm (the executable name as the kernel sees it), not against the arguments, so your own wrapper does not end up in the selection

pkill -u "$USER" -x swaync 2>/dev/null || true
pkill -u "$USER" -SIGUSR2 waybar 2>/dev/null || true

swaync and waybar are Wayland panels, and SIGUSR2 for waybar means "reload the config"

comm is truncated to 15 characters. For /usr/lib/polkit-kde-authentication-agent-1, /proc/<pid>/comm holds polkit-kde-auth, so pkill -x polkit-kde-authentication-agent-1 finds nothing

The third case is checking whether the process that the script itself started is still alive. No name lookup is needed for that: the PID of the last background job is in $!

cmd & AP=$!
sleep 1
kill -0 "$AP" 2>/dev/null && echo alive || echo dead

kill -0 sends no signal, it only checks that the process exists and is reachable. For a PID owned by someone else it also returns an error, so as an "is it alive" test this works only for your own processes

Check

The checks go from general to specific: who matches the pattern, whether a wrapper is among them, and what pgrep returned

# 1) who matches the pattern
pgrep -a -f 'authentication-agent'

# 2) did the wrapper end up in the selection
pgrep -a -f 'authentication-agent' | grep -q 'bash -c' \
  && echo 'wrapper in selection' || echo 'no wrapper'

# 3) pgrep exit code
pgrep -f 'authentication-agent'; echo "rc=$?"

The pgrep exit code: 0 means matches exist, 1 means there are none; one is not an error but a normal answer. Also check the exe filter itself against a live process

$ readlink /proc/4301/exe
/usr/lib/polkit-kde-authentication-agent-1

Reproduce the scenario in the same wrapper it failed in, via bash -c '...'. In an interactive shell the command line is shorter and the match may not appear

Pitfalls

  • comm is truncated to 15 characters, so pkill -x with a long name silently finds nothing: compare against /proc/<pid>/comm before writing the pattern
  • pkill -f catches unrelated processes with similar text in their arguments: an editor with the script open, a grep over it, a tail -f on the log
  • An empty pgrep result and "no process" are indistinguishable in a pgrep ... && kill chain: exit code 1 silently skips the whole logic and the error surfaces later
  • Between pgrep and kill the PID may be taken by another process. For a short pause the risk is low, for a long one use $! or a pid file
  • Comparing against $$ in a subshell does not exclude the current process: add $BASHPID
  • If the PID list is empty, kill $old without an emptiness check prints usage and returns 2, and under set -e that aborts the script
  • If the match is clearly foreign and you cannot tell where the process came from, walking the parent PID helps: Who calls unshare

Takeaway

-f in pgrep and pkill means "search the whole command line", not "search the program name". A command started through bash -c carries its own text in its arguments and lands in its own pattern, and square brackets help only with a single occurrence. To stop by name use pkill -x with the truncated comm, for an exact target use readlink /proc/<pid>/exe, for your own process use $! and kill -0

Need this solved on your project?