// 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
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 $?
143Code 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-1The 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-1The 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: 4388If 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"
doneReview 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"
doneFor 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 || trueswaync 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 deadkill -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-1Reproduce 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
commis truncated to 15 characters, sopkill -xwith a long name silently finds nothing: compare against/proc/<pid>/commbefore writing the patternpkill -fcatches unrelated processes with similar text in their arguments: an editor with the script open, agrepover it, atail -fon the log- An empty
pgrepresult and "no process" are indistinguishable in apgrep ... && killchain: exit code 1 silently skips the whole logic and the error surfaces later - Between
pgrepandkillthe 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 $oldwithout an emptiness check prints usage and returns 2, and underset -ethat 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
Related notes
Linux: “command not found” — PATH, ifconfig, pip
command not found on Linux: how to trace a binary through PATH, why sudo and login shells see a different path, and what replaces ifconfig and pip
Who calls unshare: intercepting a binary through PATH
A command fails without naming the culprit. Here is the trick: replace the binary with a logging script via PATH and find the caller without tracing or root
Nix on CachyOS: install impossible — run it rootless
Nix will not install when the root is read-only, sudo is broken and /nix is baked into ELF. Three limits and a rootless run with mount namespace and chroot