[BUG] Agents emit self-matching `pgrep -f` polling patterns that deadlock background tasks

Resolved 💬 3 comments Opened May 19, 2026 by gjkim42 Closed May 19, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code

What's Wrong?

When agents launch a long-running command via the Bash tool with run_in_background: true and want to wait for it to finish, they commonly improvise a polling shell like:

until [ -z "$(pgrep -f 'CMD_NAME')" ]; do sleep N; done; tail -K OUT

This pattern deadlocks. pgrep -f matches any process whose /proc/*/cmdline contains the pattern, and the shell running this command has pgrep -f 'CMD_NAME' in its own argv — so pgrep always finds the caller itself. The until condition is never empty, the loop runs forever (even after the real command has finished), the tail after the && never executes, the BashOutput tool call the agent is awaiting never returns, and claude blocks indefinitely in do_epoll_wait holding pidfds for the wedged wrappers.

The root cause is the model picking a self-matching idiom, but the absence of a first-class "wait for background task" primitive in Claude Code is what pushes models toward hand-rolled polling in the first place.

In headless sessions (Claude Code running inside a Kubernetes Job), the wedged wrapper holds claude in do_epoll_wait on the pidfd and the container never terminates.

What Should Happen?

The background-task lifecycle should not deadlock when the agent waits on it. Concretely, any of:

  • Claude Code's default system prompt warns the model away from the self-matching pgrep -f idiom.
  • A watchdog terminates a backgrounded shell whose process subtree is only sleep for >N minutes.
  • A first-class wait_for: <task_id> (or equivalent) primitive on the Bash tool removes the need for the model to hand-roll a polling loop.

Error Messages/Logs

PID 1   entrypoint.sh                           wchan: do_wait
PID 9   claude --prompt "..."                   wchan: do_epoll_wait
        ├─ FD 25 = pidfd → PID 12586
        └─ FD 27 = pidfd → PID 7251
PID 12586  bash -c "until [ -z $(pgrep -f 'make verify') ]; do sleep 5; done; tail -60 OUT"
   └─ sleep 5  (respawning forever)
PID 7251   bash -c "until [ ! -e /proc/$(pgrep -f 'go test ./internal/cli/') ]; do sleep 2; done; cat OUT | tail -30"
   └─ sleep 2  (respawning forever)

$ pgrep -af 'make verify'
# returns the wrapper bash itself + the parent claude (its argv carries the prompt text)

The real make verify finished long before — its output file ends with Verification passed. Only the polling shells hang.

Steps to Reproduce

  1. Run Claude Code non-interactively against a repo that has a long-running build command (e.g. make verify taking several minutes):

``
claude --dangerously-skip-permissions --output-format stream-json --verbose \
-p "Run \
make verify\ and \make test\, then summarize the result."
``

  1. Observe the agent launches the build via the Bash tool with run_in_background: true, then writes a second background command of the form until [ -z "$(pgrep -f 'make verify')" ]; do sleep N; done; tail OUT.
  2. The real make verify completes, but the polling shell keeps spinning. claude blocks waiting on its pidfd. The session never finishes.

This reproduces especially reliably when the user prompt itself contains the literal command name (because then claude's own argv also matches pgrep -f), but the wrapper-self-match alone is enough.

Claude Model

Opus

Is this a regression?

I don't know

Claude Code Version

2.1.143 (Claude Code)

Platform

Anthropic API

Operating System

Other Linux

Terminal/Shell

Non-interactive/CI environment

Additional Information

Suggested fixes:

  1. Default system-prompt guidance — warn against pgrep -f 'PAT' as a completion poll and point at:
  • PID capture: cmd & PID=$!; while kill -0 "$PID" 2>/dev/null; do sleep N; done
  • Self-exclusion: pgrep -f 'PAT' | grep -vw $$
  1. Background-task watchdog — if a backgrounded shell's process subtree is just sleep for >N minutes, surface a warning to the model or kill the wrapper.
  2. First-class wait primitive — a Bash tool flag like wait_for: <task_id> that handles polling correctly so the model doesn't hand-roll the loop.

Workaround we applied in our repo: kelos-dev/kelos#1165 — added a bullet to our CLAUDE.md telling the model not to use the self-matching pgrep -f form. Works for our agents, but the bug will keep recurring for other users until Claude Code's defaults change.

Environment notes:

  • Claude Code container image: ghcr.io/kelos-dev/claude-code:main (built on the official Claude Code)
  • Host: Kubernetes Job (linux/amd64)

View original on GitHub ↗

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