Cursor CLI: when a folder name was still shell code
Cursor for macOS installs shell wrapper scripts next to the app so you can open files and folders from a terminal. That is ordinary developer UX. The interesting part is how those wrappers hand arguments to the real Node/Electron CLI.
On the build I looked at, both wrappers ended in eval. Once you put untrusted data — including a
filesystem path — through eval, quoting at the call site stops protecting you. Confirmed on
Cursor 2.2.20 (commit b3573281c4775bfc6bba466bf6563d3d498d1070).
cursor / code shell wrappers. PoC proof is
open -a Calculator only. Reported to Cursor by email.
The prompt and the context
Same loop as the rest of this site: constrain the model, force a PoC, do not treat a plausible narrative as a
finding. I also fed the agent a short reference to my earlier
Harmony SASE write-up — the quote-in-the-path /
bash -c story — as an example of “path characters becoming shell syntax.” Then I pointed it at
Cursor’s own macOS install, not a third-party VPN helper.
The prompt was not “find bugs in Cursor.” It was narrower:
Authorized lab research only.
Target: Cursor for macOS app bundle and CLI entrypoints
(/Applications/Cursor.app, shell wrappers that launch the editor).
Prior work / context: path-injection into bash -c (Harmony SASE HelperTool).
Look for the same class in developer tooling — wrappers that build a shell
string from paths or argv and re-parse it.
Focus:
- shell launchers under Resources/app/bin
- eval, bash -c, system(), unsafely joined command strings
- arguments that look like file/folder paths but carry metacharacters
For each candidate:
- exact file and function
- what the caller controls (path, argv, env)
- a minimal PoC (benign side effect only)
- do not treat speculation as a finding
That is what pulled the searchlight onto
eval "$CURSOR_CLI" "$@". The model surfaced the wrapper; I confirmed it with a crafted
directory name and Calculator. Context from the Harmony post mattered — once you have watched a path break
out of quotes into a root shell, you start reading every launcher the same way.
Recon: the build under test
Before reading wrappers, I pinned the install. About Cursor → Copy:
Version: 2.2.20
VSCode Version: 1.105.1
Commit: b3573281c4775bfc6bba466bf6563d3d498d1070
Date: 2025-12-12T06:29:26.017Z
Electron: 37.7.0
Chromium: 138.0.7204.251
Node.js: 22.20.0
V8: 13.8.258.32-electron.0
OS: Darwin x64 25.1.0
Where the wrappers live
Inside that app bundle:
/Applications/Cursor.app/Contents/Resources/app/bin/cursor
/Applications/Cursor.app/Contents/Resources/app/bin/code
Symlinks from /usr/local/bin (or wherever “Shell Command: Install …” put them) usually point
here. Both scripts resolve the Electron binary and cli.js, then launch through a helper that
looked like this:
use_cursor_cli() {
if [ -n "$CURSOR_CLI" ]; then
eval "$CURSOR_CLI" "$@"
exit $?
else
echo "Error: Cursor CLI not found..." 1>&2
exit 1
fi
}
For local mode, CURSOR_CLI was built as a string roughly like:
CURSOR_CLI="ELECTRON_RUN_AS_NODE=1 \"$ELECTRON\" \"$CLI\""
So the final line was not “exec this argv array.” It was “concatenate a shell string and re-parse it.”
Why eval turns a path into a payload
"$@" preserves argument boundaries when you pass them to a normal command. Under
eval, those same arguments are joined into a new shell command line and parsed again.
Metacharacters that were safely inside one argv element — ;, &&, backticks,
$(), newlines — become syntax for the outer shell.
That matters for anything that hands a path to cursor: opening a repo from a script, a
“open in Cursor” handler, CI glue, or a teammate pasting a folder path. The delivery that interested me was
the boring one — a crafted directory name.
Proof of concept
Lab session on Darwin with Calculator as the visible side effect:
bash-3.2$ mkdir -p 'repo; open -a Calculator'
bash-3.2$ /Applications/Cursor.app/Contents/Resources/app/bin/cursor "$(pwd)/repo; open -a Calculator"
bash-3.2$
The outer quotes matter for the story. At the interactive shell, "$(pwd)/repo; open -a Calculator"
is one argument. Your shell does not treat the semicolon as a command separator. The wrapper still
runs eval on that string, re-parses the semicolon, and executes
open -a Calculator as the current user. Calculator pops. That is the whole bug.
Impact
Anyone who can influence an argument to the cursor or code wrapper — most obviously
a path under their control — gets command execution as the Cursor user. That is not a root HelperTool LPE.
It is still a real footgun on developer machines: clone or unzip a repo whose top-level folder name carries
shell metacharacters, open it with the CLI, and the payload runs in your session.
The same eval also broke honest paths with spaces, parentheses, or brackets (think SvelteKit
route groups). The security reading and the “CLI cannot open my file” reading are the same root cause.
The fix shape
Drop eval. Keep argv intact and exec directly, for example:
ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" "$@"
No second parse. Spaces stay spaces. A semicolon in a directory name stays a character in a path, not a command separator.
Disclosure
I reported this to Cursor by email. The vulnerable surface was the macOS app wrappers on
2.2.20 as above. Later Cursor builds moved off the unsafe eval wrapper path —
if you are still on an old install, update.
What I took away
If a launcher builds a shell string and then evals it, treat every argument as attacker-controlled
syntax — including “just a path.” Quoting at the call site is not a boundary once the callee re-parses.
The lesson was not “AI found a bug.” It was the same one as the OpenBSD and VirtualBox posts: a narrow prompt, a prior write-up as reference context, and an evidence bar that ends in a PoC. Context is king — here, the interesting moment is where a quoted folder path still becomes shell code because of how the wrapper was written.
Cursor is mentioned here for technical discussion; this site is not affiliated with Anysphere / Cursor. Research was conducted in an authorized lab setting.