Model repeatedly writes self-matching pgrep -f guards in check-then-launch Bash commands (esp. over SSH)

Status Open
Maintainer reply None cached
Activity 0 comments · opened Aug 13, 2026

Summary: When asked to check whether a process is running before launching it, Claude reliably reaches for a single compound command of the form:

pgrep -f <pattern> && echo "already running" || (launch ...)

Because the pattern — or the launch branch's own text — appears in the compound command's cmdline, pgrep -f matches the checking shell itself. The guard reports "already running" and the launch is silently skipped.

The SSH variant is worse. A remote check-and-launch arrives as one bash -c '<entire script>' cmdline, so everything in the remote string is visible to pgrep. Even the classic [p]attern bracket trick fails there whenever the launch text (e.g. python pull_all_responses.py) sits in the same remote string as the guard — the bracket only protects the literal it's applied to.

Impact we observed (agentic ops on a small server fleet):

  • A chained monitoring script whose pgrep guard self-matched held a death-detection data pipeline in a silent 25-hour blind window before a human noticed.
  • In a later session, the model hit the same trap twice in a row — first with a plain compound guard, then again after "fixing" it with the bracket trick — even though a postmortem of the earlier incident was in its context. The failure reads as "the job is already running," so nothing errors and nothing launches.

Why this might deserve a nudge in the Bash tool guidance or training: the failure is invisible (exit 0, plausible output), the idiom is the model's default for "check then start," and agents run far more unattended check-then-launch shell than humans do. A sentence in the Bash tool prompt along the lines of "process-existence checks must not share a command line (or SSH remote string) with the launch they guard; prefer flock/pidfiles for do-not-double-run semantics" would likely eliminate the class.

Workaround we settled on: a small is-running wrapper — pgrep -af "$1" filtered to exclude the checker's own PID and full ancestor chain — plus a rule that guards and launches never share a remote command string.

(Filed on GitHub because /bug currently fails with a 403 from the feedback server — mentioning in case that's its own issue.)

View original on GitHub ↗