This solution proposed by cursor works for me:
Cursor Remote-SSH Bug: Connection Fails Through JumpServer (Bastion Host)
Environment
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:
- Generate a random marker ID (e.g.,
02054a02fa6c1af96f94eafb)
- Pipe the install script via:
cat "script.sh" | ssh -T -D <port> sg-dev bash --login -c bash
- Wait for
MARKER_ID: end to appear in stdout (via regex match)
- 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:
- 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.
- Use
lastIndexOf for the start marker to prefer the real output over echoed content.
- 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