[BUG] Agents emit self-matching `pgrep -f` polling patterns that deadlock background tasks
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 -fidiom. - A watchdog terminates a backgrounded shell whose process subtree is only
sleepfor >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
- Run Claude Code non-interactively against a repo that has a long-running build command (e.g.
make verifytaking several minutes):
``make verify\
claude --dangerously-skip-permissions --output-format stream-json --verbose \
-p "Run \ and \make test\, then summarize the result."``
- Observe the agent launches the build via the
Bashtool withrun_in_background: true, then writes a second background command of the formuntil [ -z "$(pgrep -f 'make verify')" ]; do sleep N; done; tail OUT. - The real
make verifycompletes, but the polling shell keeps spinning.claudeblocks 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:
- 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 $$
- Background-task watchdog — if a backgrounded shell's process subtree is just
sleepfor >N minutes, surface a warning to the model or kill the wrapper. - 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)
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗