[BUG] `Monitor` `until`-loop self-matches when model uses `pgrep -f "<cmdline>"` as its check

Resolved 💬 4 comments Opened Apr 23, 2026 by the-exodus Closed Jun 19, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Monitor until-loop self-matches when model uses pgrep -f "<cmdline>" as its check

Environment

  • Claude Code (Opus, but pattern is model-agnostic — generated bash is the same)
  • Linux, bash
  • Monitor tool with the documented polling pattern (until <check>; do sleep N; done)

What happens

When the model invokes Monitor with a pgrep-style check like:

until ! pgrep -f "dotnet test <args>" > /dev/null; do sleep 20; done

…the Monitor tool spawns the loop inside a bash -c '<the whole until expression>'. That bash's own argv contains the literal string the check is searching for. pgrep -f matches against the full command line, so it finds the Monitor's own bash process. pgrep returns 0, ! negates to false, and the loop never exits — even after the real background command has completed long ago.

Evidence from a hung session

$ ps --ppid <claude_pid> -o pid,cmd
 PID  CMD
N    /usr/bin/bash -c ... eval 'until ! pgrep -f "dotnet test <args>" > /dev/null; do sleep 20; done; ...'

$ cat /proc/N/wchan
do_wait            # blocked on the child sleep

$ pgrep -af "dotnet test <args>"
N /usr/bin/bash -c ... 'until ! pgrep -f "dotnet test <args>" ...'
#   ^ the Monitor bash matches itself

# The actual background command finished hours ago; no other process matches.

Claude Code's parent process cannot exit because it's still awaiting the Monitor tool result, which will never arrive. In headless mode (claude -p), this hangs the entire invocation indefinitely.

Why it matters

This is a plausible default check for models instructed to "wait for a background command to finish." The self-match trap is subtle — it's not obvious from reading the generated bash, and the surface symptom ("claude hangs forever") is easy to misattribute to MCP teardown or network issues. I wasted time chasing those red herrings before tracing it.

Suggested directions

Any of these would fix it; they're not mutually exclusive:

  1. Monitor tool implementation: invoke the check through a wrapper that doesn't leak the pattern into the shell's argv. e.g. write the check command to a temp file and bash <file>, or pass the pattern via an environment variable and have the check read it from $CHECK_PATTERN. Either prevents pgrep -f self-matching.
  2. Tool description: explicitly warn against pgrep -f "<cmdline>" as a check and recommend alternatives: kill -0 <pid>, tail -f <output> | grep -m1 <marker>, or a sentinel file. Many models will default to pgrep -f otherwise because it's the most natural phrasing.
  3. Monitor tool safety: apply a reasonable max wall-clock timeout on the outer until loop so a self-matching check eventually fails rather than hanging forever.

Minimal repro

Ask an agent (any model) to run a short background command and then use Monitor to wait for it using pgrep -f "<the exact command>". The Monitor never exits even after the command completes.

What Should Happen?

Monitoring a process should not hang claude.

Error Messages/Logs

Steps to Reproduce

Minimal repro

Ask an agent (any model) to run a short background command and then use Monitor to wait for it using pgrep -f "<the exact command>". The Monitor never exits even after the command completes.

Claude Model

Opus

Is this a regression?

Yes, this worked in a previous version

Last Working Version

_No response_

Claude Code Version

2.1.117 (Claude Code)

Platform

Anthropic API

Operating System

Other Linux

Terminal/Shell

Other

Additional Information

_No response_

View original on GitHub ↗

This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗