Remote-ssh: connect to remote host failed

Where does the bug appear (feature/product)?

Cursor IDE

Describe the Bug

connect to remote host failed using latest “anysphere.remote-ssh” plugin (version: 1.0.39)

Steps to Reproduce

  1. use jumpserver in ~/.ssh/confg:
    Host my_server
    HostName jumpserver.mydomain.com
    User name@root@ip
    ForwardAgent yes

  2. Cursor IDE ssh button → connect to host → my_server

Operating System

MacOS

Version Information

Version: 2.4.31
VSCode Version: 1.105.1
Commit: 3578107fdf149b00059ddad37048220e41681000
Date: 2026-02-08T07:42:24.999Z
Build Type: Stable
Release Track: Default
Electron: 39.2.7
Chromium: 142.0.7444.235
Node.js: 22.21.1
V8: 14.2.231.21-electron.0
OS: Darwin arm64 24.2.0

Does this stop you from using Cursor

Yes - Cursor is unusable

This solution proposed by cursor works for me:

Cursor Remote-SSH Bug: Connection Fails Through JumpServer (Bastion Host)

Environment

  • Cursor Extension: anysphere.remote-ssh-1.0.39
  • Local OS: macOS (darwin 24.2.0)
  • Remote OS: Linux (CentOS/RHEL, x86_64)
  • SSH Intermediary: JumpServer bastion host (FIT2CLOUD JumpServer)
  • SSH Config:
    Host sg-dev
        HostName jumpserver.mydomain.com
        User user@root@ip
        ForwardAgent yes
    

Symptom

Cursor fails to connect to a remote server through a JumpServer bastion host with error:

Error installing server: Couldn't install Cursor Server, install script returned non-zero exit status
Server install command exit code: 1

However:

  • VSCode Remote-SSH connects to the same host successfully.
  • Command-line ssh sg-dev works fine.
  • The remote Cursor server actually installs and starts correctly (the logs show exitCode==0== at the end), but Cursor gives up before that point.

Root Cause

Background

The JumpServer bastion host forces TTY/interactive mode on SSH sessions regardless of the -T flag. This causes the remote shell to echo back all stdin content as stdout, including the entire install script.

The Bug

Cursor’s install flow works as follows:

  1. Generate a random marker ID (e.g., 02054a02fa6c1af96f94eafb)
  2. Pipe the install script via: cat "script.sh" | ssh -T -D <port> sg-dev bash --login -c bash
  3. Wait for MARKER_ID: end to appear in stdout (via regex match)
  4. Parse output between MARKER_ID: start and MARKER_ID: end to extract results (exitCode, ports, etc.)

The install script itself contains the marker strings inside function definitions:

print_install_results_and_exit() {
    echo "02054a02fa6c1af96f94eafb: start"
    echo "exitCode==$1=="
    echo "nodeExecutable==$SERVER_NODE_EXECUTABLE=="
    ...
    echo "02054a02fa6c1af96f94eafb: end"
    exit $1
}

When the JumpServer echoes the script content back to stdout, these marker strings appear in the output before the script actually executes.

Bug 1 — Premature trigger (dist/main.js):

const ye = new RegExp(`${de}: end`, "m");

This regex matches MARKER_ID: end as a substring inside the echoed echo "MARKER_ID: end" line. The promise resolves immediately upon seeing the echoed script content, long before the script finishes executing.

Bug 2 — Wrong match in parser (dist/main.js, function L):

function L(e, t) {
    const r = `${t}: start`, n = `${t}: end`;
    const o = e.indexOf(r);  // Finds FIRST occurrence (in echoed content)
    ...
}

indexOf finds the marker in the echoed script content (e.g., inside echo "MARKER_ID: start"). The “parsed” content between the markers is function definition code like echo "exitCode==$1==", not actual results like exitCode==0==.

Since the key-value parser splits by ==, it gets garbled keys like echo "exitCode instead of exitCode. The actual exitCode field ends up undefined, and the fallback logic:

const Re = parseInt(be.exitCode ?? "1", 10);  // undefined → "1" → 1

defaults to exit code 1, causing the connection to fail.

Fix

Two minimal changes in ~/.cursor/extensions/anysphere.remote-ssh-1.0.39/dist/main.js:

Fix 1: Anchor regex to line start

Prevents premature matching on echoed content like echo "MARKER: end".

- new RegExp(`${de}: end`,"m")
+ new RegExp(`^${de}: end`,"m")

The ^ anchor ensures the marker is only matched when it appears at the beginning of a line — which is the case for actual script output (MARKER: end\n), but NOT for echoed script content ( echo "MARKER: end"\n).

Fix 2: Use lastIndexOf instead of indexOf in parser

Ensures the parser picks up the last (real) occurrence of the markers, not the first (echoed) one.

  function L(e,t){
      const r=`${t}: start`,n=`${t}: end`,
-     o=e.indexOf(r);
+     o=e.lastIndexOf(r);
      if(o<0)return;
      const s=e.indexOf(n,o+r.length);
      if(s<0)return;

How to Apply

cd ~/.cursor/extensions/anysphere.remote-ssh-1.0.39/dist

# Backup
cp main.js main.js.bak

# Fix 1: Anchor the end-marker regex to line start
sed -i.tmp 's/new RegExp(`${de}: end`,"m")/new RegExp(`^${de}: end`,"m")/' main.js

# Fix 2: Use lastIndexOf in parser function L
sed -i.tmp 's/function L(e,t){const r=`${t}: start`,n=`${t}: end`,o=e.indexOf(r)/function L(e,t){const r=`${t}: start`,n=`${t}: end`,o=e.lastIndexOf(r)/' main.js

# Cleanup
rm -f main.js.tmp

# Restart Cursor (Cmd+Q then reopen)

How to Revert

cd ~/.cursor/extensions/anysphere.remote-ssh-1.0.39/dist
cp main.js.bak main.js
# Restart Cursor

Suggested Permanent Fix for Cursor Team

The core issue is that the marker-matching logic assumes stdout only contains execution output. When an intermediary (JumpServer, bastion host, or any SSH proxy that forces TTY) echoes back the piped script content, the markers appear twice — once in the echo and once in the real output.

A robust fix could be:

  1. Anchor markers to line boundaries in both the trigger regex and the parser — markers should only match when they appear on their own line, not embedded inside echo "..." commands.
  2. Use lastIndexOf for the start marker to prefer the real output over echoed content.
  3. Consider a more unique marker format that is less likely to appear as a substring in echoed script content (e.g., include a newline prefix in the marker pattern, or use a format that wouldn’t survive shell echo like a null byte prefix).

Reproduction

Any SSH connection through a bastion host / JumpServer that forces TTY allocation (ignoring ssh -T) will trigger this bug. The JumpServer echoes all piped stdin back as stdout, causing the install script’s marker strings to be matched prematurely.

Common in enterprise environments using:

  • FIT2CLOUD JumpServer
  • Other bastion host solutions with custom SSH server implementations

Hey, thanks for the report. This is an incredibly thorough root-cause analysis, and the debugging is genuinely impressive.

Both changes make perfect sense: anchoring the regex with ^ to avoid matching markers inside echo "..." lines, and using lastIndexOf to capture the real output instead of the contents of the echo script.

We’ve hit the exact same issue before with JumpServer bastions (see this post: Connection to Cursor server failed: Couldn't install Cursor Server, install script returned non-zero exit status), and your write-up finally clarifies the root cause.

I’m passing this along to the extensions team along with your suggested fix. There’s no release timeline yet, but the level of detail you provided should make it much easier to implement. Your temporary workaround is great.

Let me know if anything new comes up.

This topic was automatically closed 22 days after the last reply. New replies are no longer allowed.