Shell tool hangs indefinitely when running rg (ripgrep) recursive directory search

Where does the bug appear (feature/product)?

Cursor IDE

Describe the Bug

Running rg <pattern> (ripgrep recursive directory search) through the Shell tool never completes. The tool reports the command was “sent to background” after the 30-second timeout, but the terminal output file shows zero output and the command appears to hang indefinitely. The rg process itself exits correctly in under 1 second — this is a process-exit detection bug in the Shell tool infrastructure, not an AI model issue.

Steps to Reproduce

  1. Open any git repo with source files
  2. In Agent mode, have the agent run a bare ripgrep command: rg -i somepattern
  3. Observe: the Shell tool times out after 30s and backgrounds the command
  4. Check the terminal output file — it shows zero output and no exit code, even though rg finished instantly

Expected Behavior

Command completes in under 1 second (as it does in a real terminal or when wrapped in sh -c).

Operating System

Linux

Version Information

Version: 2.6.19 (system setup)
VSCode Version: 1.105.1
Commit: 224838f96445be37e3db643a163a817c15b36060
Date: 2026-03-12T04:07:27.435Z
Build Type: Stable
Release Track: Default
Electron: 39.4.0
Chromium: 142.0.7444.265
Node.js: 22.22.0
V8: 14.2.231.22-electron.0
OS: Windows_NT x64 10.0.26200

For AI issues: which model did you use?

Claude Opus 4.5 — but this is not model-dependent. It’s a Shell tool infrastructure bug. The model correctly issues the rg command; the Shell tool fails to detect when rg exits.

Additional Information

Detailed diagnosis:

Command Result
rg --version Works
rg -i pattern single_file.cpp Works
rg -i pattern (recursive) Hangs forever
sh -c 'rg -i pattern' Works
bash -c 'rg -i pattern' Works
timeout 5 rg -i pattern Works
strace -f rg -i pattern Works
rg -i pattern & wait $! Works
grep -ri pattern Works

Key findings:

  • Shell tool’s persistent shell is non-interactive zsh with stdin/stdout as sockets (not pty)
  • strace confirms rg calls exit_group(1) cleanly; all worker threads exit normally
  • The bug only manifests when rg is the direct foreground process and produces no stdout output
  • Any wrapper (sh -c, timeout, strace, background+wait) causes normal completion

Hypothesis: The Shell tool’s process-exit detection doesn’t fire when a multithreaded Rust binary using exit_group() is the direct foreground child with no stdout output over a socket.

Workaround: Use the built-in Grep tool (which works perfectly), or wrap rg in sh -c '...'.

Does this stop you from using Cursor

No - Cursor works, but with this issue

Thanks for the thorough bug report! I was able to reproduce this on macOS as well, so it’s not platform-specific.

In the meantime, besides the workarounds you already mentioned, passing an explicit path works reliably as a workaround: rg -i pattern . instead of rg -i pattern.

This has been filed internally and we’ll follow up here with any updates.

Thank Cursor for the detailed analysis and bug report itself! I’m not sure I changed a single character

Where does the bug appear (feature/product)?

Cursor IDE

Describe the Bug

When Cursor’s agent runs rg (ripgrep) via its shell tool without an explicit path argument, the process hangs indefinitely instead of completing in seconds.

Root cause: rg’s stdin heuristic. When no path argument is given, ripgrep checks whether stdin is a TTY. If it is, it searches the current working directory; if it isn’t, it reads from stdin instead. The agent’s shell wraps every command in cursorsandbox with stdin connected to a Unix domain socket, so the TTY check fails and rg silently waits forever for input on stdin that never arrives.

Symptoms:

  • The rg process sits at ~0.0% CPU in state S (sleeping), accumulating essentially no CPU time, while the agent’s shell tool reports it as “still running” and eventually backgrounds it.
  • lsof shows FD 0 as a Unix socket, not a TTY.
  • The exact same command run in a regular interactive Terminal in the same directory finishes in ~1–2 seconds.
  • Appending . (or any explicit path) to the rg invocation fixes the hang.

This is a high-frequency footgun in real agent sessions because the agent often shells out to rg directly (which Cursor’s tool docs actively recommend over grep). The built-in Grep tool is unaffected because it implicitly passes the workspace root as the path.

Steps to Reproduce

  1. Open Cursor in a moderately sized repo (I reproduced in a ~100k-file Node monorepo, but any non-trivial repo works).

  2. In an Agent chat, ask the agent to run an rg command without a path argument, e.g.:

    rg -c "assert\.ok\(" --type js -g "*.spec.js"
    
  3. Observe that the command never returns. The shell tool reports it as still running and eventually moves it to background.

  4. Open a regular macOS Terminal, cd to the same directory, and run the exact same command. It completes in ~1–2 seconds.

  5. (Optional) While the agent’s rg is hung, inspect it:

    ps -o pid,pcpu,time,stat -p <rg_pid>     # ~0.0% CPU, state S, ~0:00.00 CPU time
    lsof -p <rg_pid> | awk '$4 ~ /^0/'       # FD 0 is "unix", not a TTY
    
  6. Re-run the same command via the agent but with . appended:

    rg -c "assert\.ok\(" --type js -g "*.spec.js" .
    

    It completes immediately with correct results, confirming the hang is rg’s stdin fallback, not anything else about how the agent runs the command.

Expected Behavior

rg invoked by the agent should behave the same as rg invoked from a normal terminal: when no path argument is given, it should search the current working directory and return results in seconds, not block forever on a stdin that will never produce data.

Any of the following would resolve it:

  • Wire stdin to /dev/null (or close it) for commands run by the shell tool, so rg sees EOF immediately and falls through to searching cwd.
  • Allocate a pty for the shell tool so rg’s TTY check succeeds.
  • Update the agent’s tool documentation/system prompt to instruct it to always pass an explicit path when invoking rg directly. (The shell tool docs already steer the agent toward rg over grep; adding a sentence about always passing a path would be a small change.)

Operating System

MacOS

Version Information

Version: 3.3.30 (Universal)
VSCode Version: 1.105.1
Commit: 3dc559280adc5f931ade8e25c7b85393842acf30
Date: 2026-05-09T18:28:42.332Z
Layout: editor
Build Type: Stable
Release Track: Default
Electron: 39.8.1
Chromium: 142.0.7444.265
Node.js: 22.22.1
V8: 14.2.231.22-electron.0
OS: Darwin arm64 25.5.0

For AI issues: which model did you use?

Most recent issue seen on claude-opus-4-7-thinking-xhigh, but I’m almost 100% certain I’ve seen this across many different models

Additional Information

Environment

  • ripgrep: 15.1.0
  • Default shell: zsh

Evidence the process is blocked on stdin, not slow

A stuck rg invoked by the agent had been running for 19 minutes when inspected; it had accumulated essentially no CPU time:

PID    ELAPSED  %CPU  TIME      STAT
3096   19:23    0.0   0:00.01   S

lsof on the same PID confirms FD 0 is a Unix domain socket, not a TTY:

rg  3096  user  0u  unix 0x7a1c8e2ecfa67e52  ->0x1be40e439438bbc

For comparison, the same rg invocation run by hand in an interactive Terminal in the same cwd completed in 1.81s and produced the expected results.

Process tree (from ps)

The agent’s command is wrapped roughly like this (truncated):

/Applications/Cursor.app/.../helpers/cursorsandbox --policy /var/folders/.../sandbox-policy-xxx -- /bin/zsh -c "<wrapper script> -- <user command>"
 └─ /bin/zsh -c "..."
     └─ rg -c "assert\.ok\(" --type js -g "*.spec.js" -g "integration-tests/**"

Stdin to the inner zsh (and therefore to rg) is a unix socket inherited from the cursorsandbox wrapper, which is what trips rg’s “stdin is not a TTY → read from stdin” heuristic.

Why this is more than a minor papercut

  • The shell tool’s own description tells the agent to use rg instead of grep, but doesn’t tell it to pass a path. So the agent reaches for rg constantly and falls into this trap.
  • Each hang typically wastes ~minutes per occurrence (the agent waits, then backgrounds, then sometimes retries), and consumes a model turn slot in the meantime.
  • The built-in Grep tool is unaffected because it implicitly passes the workspace root as the path, which suggests the cleanest fix is either matching that behavior at the shell-tool layer (e.g. close/redirect stdin to /dev/null) or instructing the agent in the tool description.

Workaround currently used
A workspace rule telling the agent to always append . (or another explicit path) when shelling out to rg.

Does this stop you from using Cursor

No - Cursor works, but with this issue

I would argue that this is the best solution. It’s easy to implement, always gives the expected behavior, and generalizes across any other command the shell tool would execute. It’s arguably the most “correct” path forward.