-
Notifications
You must be signed in to change notification settings - Fork 2.3k
fix(windows): detect cursor bundled node process #3034
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -378,7 +378,9 @@ fn wrapped_agent_name_from_runtime_argv(runtime: &str, argv: Option<&[String]>) | |
| let runtime_name = normalized_agent_lookup_name(path_basename(runtime)); | ||
|
|
||
| match runtime_name.as_str() { | ||
| "node" | "bun" => script_arg_agent_name(argv, &["-e", "--eval", "-p", "--print"], &[]), | ||
| "node" => cursor_agent_name_from_bundled_node_argv(argv) | ||
| .or_else(|| script_arg_agent_name(argv, &["-e", "--eval", "-p", "--print"], &[])), | ||
| "bun" => script_arg_agent_name(argv, &["-e", "--eval", "-p", "--print"], &[]), | ||
| name if is_python_runtime(name) => script_arg_agent_name(argv, &["-c"], &["-m"]), | ||
| "sh" | "bash" | "zsh" | "fish" => script_arg_agent_name(argv, &["-c"], &[]), | ||
| "cmd" => windows_cmd_arg_agent_name(argv), | ||
|
|
@@ -388,6 +390,36 @@ fn wrapped_agent_name_from_runtime_argv(runtime: &str, argv: Option<&[String]>) | |
| } | ||
| } | ||
|
|
||
| fn cursor_agent_name_from_bundled_node_argv(argv: &[String]) -> Option<String> { | ||
| let (runtime_parent, runtime_name) = path_parent_and_basename(argv.first()?)?; | ||
| let (script_parent, script_name) = path_parent_and_basename(argv.get(1)?)?; | ||
| if !runtime_name.eq_ignore_ascii_case("node.exe") | ||
| || !script_name.eq_ignore_ascii_case("index.js") | ||
| || !runtime_parent.eq_ignore_ascii_case(script_parent) | ||
| { | ||
| return None; | ||
| } | ||
|
|
||
| let mut tail = runtime_parent | ||
| .rsplit(['/', '\\']) | ||
| .filter(|component| !component.is_empty()); | ||
| let (Some(version), Some(versions), Some(package)) = (tail.next(), tail.next(), tail.next()) | ||
| else { | ||
| return None; | ||
| }; | ||
| (package.eq_ignore_ascii_case("cursor-agent") | ||
| && versions.eq_ignore_ascii_case("versions") | ||
| && !version.trim().is_empty()) | ||
|
Comment on lines
+410
to
+412
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an unrelated Knowledge Base Used: Agent detection, Ghostty integration, and configuration
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. disagree: Herdr process identity is signature-based rather than tied to a trusted install root. This requires the exact bundled node.exe/index.js pair in one cursor-agent/versions directory; requiring the default AppData root would break redirected LOCALAPPDATA. A process deliberately recreating the full official bundle signature is no more distinguishable than one naming itself cursor-agent. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair point — the signature is more constrained than my initial comment implied. The conjunction of The threat model here is also relevant: this is process identification for tracking, not a trust boundary, so the bar for "specific enough" is whether false positives are plausible in normal operation — not whether the path is cryptographically verified. I'll withdraw the concern. The current implementation is appropriate for Herdr's signature-based identity model. Should I remember this as a rule: "Herdr process detection is signature-based; do not flag detectors for lacking a trusted install-root anchor, as path anchoring would break redirected LOCALAPPDATA and is outside the threat model"? |
||
| .then(|| agent_label(Agent::Cursor).to_string()) | ||
| } | ||
|
|
||
| fn path_parent_and_basename(path: &str) -> Option<(&str, &str)> { | ||
| let split = path.rfind(['/', '\\'])?; | ||
| let parent = path[..split].trim_end_matches(['/', '\\']); | ||
| let basename = &path[split + 1..]; | ||
| (!parent.is_empty() && !basename.is_empty()).then_some((parent, basename)) | ||
| } | ||
|
|
||
| fn windows_cmd_arg_agent_name(argv: &[String]) -> Option<String> { | ||
| let mut args = argv.iter().skip(1); | ||
| while let Some(arg) = args.next() { | ||
|
|
@@ -894,6 +926,62 @@ mod tests { | |
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn identify_agent_in_job_detects_windows_cursor_install() { | ||
| let job = crate::platform::ForegroundJob { | ||
| process_group_id: 123, | ||
| processes: vec![foreground_process( | ||
| 123, | ||
| "node.exe", | ||
| &[ | ||
| r"C:\Users\user\AppData\Local\cursor-agent\versions\2026.08.11-e8db854\node.exe", | ||
| r"C:\Users\user\AppData\Local\cursor-agent\versions\2026.08.11-e8db854\index.js", | ||
| ], | ||
| )], | ||
| }; | ||
|
|
||
| assert_eq!( | ||
| identify_agent_in_job(&job), | ||
| Some((Agent::Cursor, "cursor".to_string())) | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn identify_agent_in_job_ignores_invalid_windows_cursor_install_paths() { | ||
| for script in [ | ||
| r"C:\Users\user\AppData\Local\cursor-agent\versions\2026.08.11-e8db854\scripts\postinstall.js", | ||
| r"C:\Users\user\AppData\Local\cursor-agent\versions\2026.08.11-e8db854\index", | ||
| r"C:\Users\user\AppData\Local\cursor-agent\versions\2026.08.11-e8db854\index.exe", | ||
| ] { | ||
| let job = crate::platform::ForegroundJob { | ||
| process_group_id: 123, | ||
| processes: vec![foreground_process( | ||
| 123, | ||
| "node.exe", | ||
| &[ | ||
| r"C:\Users\user\AppData\Local\cursor-agent\versions\2026.08.11-e8db854\node.exe", | ||
| script, | ||
| ], | ||
| )], | ||
| }; | ||
|
|
||
| assert_eq!(identify_agent_in_job(&job), None, "script: {script}"); | ||
| } | ||
|
|
||
| let lookalike = crate::platform::ForegroundJob { | ||
| process_group_id: 123, | ||
| processes: vec![foreground_process( | ||
| 123, | ||
| "node.exe", | ||
| &[ | ||
| r"C:\Program Files\nodejs\node.exe", | ||
| r"C:\workspace\cursor-agent\versions\test\index.js", | ||
| ], | ||
| )], | ||
| }; | ||
| assert_eq!(identify_agent_in_job(&lookalike), None); | ||
| } | ||
|
|
||
| #[test] | ||
| fn identify_agent_in_job_prefers_recognized_process_group_leader() { | ||
| let job = crate::platform::ForegroundJob { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.