Where does the bug appear (feature/product)?
Cursor IDE
Describe the Bug
Symptoms
In a single agent chat, tool-call shell commands get measurably slower over the course of the conversation, even for trivial commands:
| after N agent tool calls | wall-clock for echo 1 |
|---|---|
| 0 (fresh chat) | ~170 ms |
| ~5 | ~180 ms |
| ~8 | ~900 ms |
| ~10 | ~7 s |
| ~12 | ~60 s, Cursor reports exit_code: unknown |
A representative stuck call in the tool-call record:
command: "echo \"SEQ=4\""
running_for_ms: 60017
exit_code: unknown
elapsed_ms: 64704
The user command itself has finished essentially instantly — output is visible — but the wrapper shell keeps running. ps waux during one of the hangs:
%CPU %MEM RSS COMMAND
100.0 5.1 6866880 /bin/bash -O extglob -c 'snap=$(command cat <&3) && … eval "$snap" && … eval "$1"; … dump_bash_state >&4 …' -- echo "SEQ=4"
i.e. the per-call wrapper bash is at 100% CPU on ~6.8 GB RSS while processing an echo.
Behaviour is cumulative within a window. Starting a new chat resets it; Fork Chat does not reset it; a full IDE restart does.
What’s happening
Every agent tool call runs through a wrapper that restores a saved shell snapshot, runs the user command, then re-serializes the whole shell state to pass to the next call:
snap=$(command cat <&3)
builtin shopt -s extglob
builtin eval -- "$snap" # restore env / functions / aliases / opts
…
builtin eval "$1" # user command
dump_bash_state >&4 # re-serialize for next call
dump_bash_state captures, among other things, the full function table via declare -f and base64-encodes it into a snapshot variable that is replayed into the next call’s eval.
The problem: declare -f output is not guaranteed to be a fixed point under re-parse. Specifically, if any function body contains a $'…' ANSI-C-quoted literal with embedded \' or \\, bash re-escapes the already-escaped sequences each round-trip, roughly tripling the function’s serialized size. After ~10 trips that’s the difference between a 1 KB function and a ~100 MB function.
This is reproducible in plain bash, no Cursor involved — loop body=$(declare -f foo); unset -f foo; eval "$body" on any such function:
size[0] = 1408
size[1] = 1540 (×1.09)
size[2] = 1948 (×1.26)
size[3] = 3184 (×1.63)
size[4] = 6904 (×2.17)
size[5] = 18076 (×2.62)
size[6] = 51604 (×2.85)
size[7] = 152200 (×2.95)
size[8] = 454000 (×2.98) # asymptotic factor ≈ 3
Because Cursor performs exactly that round-trip on every tool call, any function in the captured shell state that has this shape becomes a time bomb: unnoticeable for the first few calls, then catastrophic around call 10–15.
A secondary amplifier: the wrapper’s own sentinel variables (snap, cursor_snap_FUNCTIONS_B64, cursor_snap_ENV_VARS_B64, cursor_snap_BASH_OPTS_B64, cursor_snap_POSIX_OPTS_B64, cursor_snap_ALIASES_B64) are not unset before the next dump, so each snapshot ends up containing the base64 of the previous snapshot, which accelerates growth independently of what’s in the user’s environment.
Measured growth from a real window
Captured inside a single agent chat, re-measuring every few tool calls:
| tool calls | snap size |
FUNCTIONS_B64 |
total set output |
|---|---|---|---|
| 0 | — | — | 1.46 MB |
| ~5 | 656 KB | 557 KB | 1.83 MB |
| ~8 | 2.70 MB | 2.02 MB | 11.6 MB |
| ~10 | 19.7 MB | 14.8 MB | 92.3 MB |
Biggest individual functions at call ~11, by declare -f byte size:
110,009,548 __rvm_unload
14,349,386 __rvm_unset_exports
4,784,110 gemset_pristine
71,200 nvm
30,534 __rvm_parse_args
3,314 dump_bash_state
About the specific triggers: in this particular capture the amplifier was a handful of RVM functions whose bodies contain auto-generated
$'…\'…'literals. I don’t want to make this a report about RVM — the mechanism is generic. Any function body, from any tool the user has loaded (version managers, direnv hooks, custom prompt helpers, shell framework plugins like oh-my-bash/zsh, hand-rolled aliases-as-functions, etc.), that contains tricky ANSI-C quoting will eventually hit this. RVM just happens to hit it fast because its functions carry dense single-quote/backslash sequences.nvmis visibly growing in the same table but three orders of magnitude slower; given enough tool calls in one chat, it would get there too.
Workaround
For the user: identify the largest offenders with
declare -F | awk '{print $3}' | while read f; do
printf '%10d %s\n' "$(declare -f "$f" | wc -c)" "$f"
done | sort -rn | head
in an agent shell session, then unset -f those functions (or re-source the tool they came from in a way that doesn’t redefine them) from your rc files. That works, but it requires the user to diagnose a wrapper-internal growth pattern from the outside, which is a big ask.
Steps to Reproduce
- A shell environment that defines at least one function whose body includes
$'…\'…'-style ANSI-C literals. In practice this is satisfied by any of:- RVM installed via its standard installer
- Any of a number of shell plugin frameworks
- Hand-written helpers that quote embedded single quotes that way
- Open Cursor, start an agent chat.
- Issue a dozen-plus sequential trivial shell tool calls (
echo 1,echo 2, …). - Observe
elapsed_msin the tool-call records climb roughly geometrically; around call 12–15 it crosses ~60 s and Cursor reportsexit_code: unknown. - In a separate terminal during one of the hangs,
ps waux | grep extglobshows the wrapper bash at 100 % CPU with RSS in the hundreds-of-MB to multi-GB range.
Operating System
MacOS
Version Information
- macOS 14.6 (Darwin 24.6.0), Apple Silicon
- Cursor 3.1.15
/bin/bash3.2.x (Apple-shipped)- The wrapper shell is launched non-interactively (
bash -O extglob -c …), but the snapshot it restores was captured from a shell that had sourced the user’s rc files, so whatever tooling the user normally has in their shell ends up inside each per-call wrapper.
Does this stop you from using Cursor
Sometimes - I can sometimes use Cursor