{"record":{"id":"0aad2509304951ec","repo":"can1357/oh-my-pi","slug":"ssh-reverse-forward-to-config-sshtarget-exited","errorCode":null,"errorMessage":"ssh reverse forward to ${config.sshTarget} exited with code ${early}","messagePattern":"ssh reverse forward to (.+?) exited with code (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/blob-broker/exposure.ts","lineNumber":511,"sourceCode":"\t\t\t\t[\n\t\t\t\t\tbinary,\n\t\t\t\t\t\"-o\",\n\t\t\t\t\t\"BatchMode=yes\",\n\t\t\t\t\t\"-o\",\n\t\t\t\t\t\"ExitOnForwardFailure=yes\",\n\t\t\t\t\t\"-N\",\n\t\t\t\t\t\"-R\",\n\t\t\t\t\t`${remotePort}:127.0.0.1:${port}`,\n\t\t\t\t\tconfig.sshTarget,\n\t\t\t\t],\n\t\t\t\t{ env: process.env, stdin: \"ignore\", stdout: \"ignore\", stderr: \"ignore\", cwd: os.homedir() },\n\t\t\t);\n\t\t\tconst early = await Promise.race([\n\t\t\t\tproc.exited.then(code => code),\n\t\t\t\tBun.sleep(SSH_READY_GRACE_MS).then(() => null),\n\t\t\t]);\n\t\t\tif (early !== null) {\n\t\t\t\tthrow new Error(`ssh reverse forward to ${config.sshTarget} exited with code ${early}`);\n\t\t\t}\n\t\t\tlogger.debug(\"blob-broker: ssh reverse forward established\", {\n\t\t\t\ttarget: config.sshTarget,\n\t\t\t\tremotePort,\n\t\t\t\tlocalPort: port,\n\t\t\t});\n\t\t\treturn processExposure(\"ssh\", normalizeBaseUrl(config.publicBaseUrl), proc);\n\t\t}\n\t}\n}\n","sourceCodeStart":493,"sourceCodeEnd":522,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/blob-broker/exposure.ts#L493-L522","documentation":"ssh prints nothing on success, so the adapter races proc.exited against a 1.5s grace period (SSH_READY_GRACE_MS): if the ssh process is still alive after the grace window, the forward is assumed established. If ssh instead exits within that window, this error is thrown with its exit code — meaning authentication failed, the host was unreachable, the remote port was refused, or ssh options were rejected.","triggerScenarios":"startExposure kind \"ssh\" where the spawned `ssh -R <remotePort>:localhost:<port> <sshTarget>` process terminates within 1.5 seconds of launch.","commonSituations":"ssh key has a passphrase and no agent, prompting fails non-interactively; known_hosts/host-key prompt blocks and ssh exits; remote sshd has AllowTcpForwarding disabled; target hostname typo or DNS failure; remote port already bound on the server; ssh binary missing or the network blocks port 22.","solutions":["Test `ssh -o BatchMode=yes -R 8787:localhost:0 user@host true` manually to see the real ssh error.","Set up key-based auth and an ssh-agent so ssh never prompts.","Check the remote sshd config allows TCP forwarding (AllowTcpForwarding yes) and the remote port is free.","Verify sshTarget hostname/port and that outbound ssh connectivity exists.","Use ssh options like -o StrictHostKeyChecking=accept-new / ServerAliveInterval for headless reliability."],"exampleFix":"// before\nproc spawn args without options → ssh prompts for host key and exits\n// after\n\"exposure\": { \"kind\": \"ssh\", \"sshTarget\": \"user@host\", \"options\": { \"sshOptions\": [\"-o\", \"StrictHostKeyChecking=accept-new\"] } } // plus working key auth","handlingStrategy":"try-catch","validationCode":"const probe = Bun.spawnSync([\"ssh\", \"-o\", \"BatchMode=yes\", \"-o\", \"ConnectTimeout=5\", sshTarget, \"true\"], { stderr: \"pipe\" });\nif (probe.exitCode !== 0) throw new Error(`ssh to ${sshTarget} unavailable: ${probe.stderr.toString()}`);","typeGuard":null,"tryCatchPattern":"try {\n  const exposure = await startExposure(config);\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith(\"ssh reverse forward\")) {\n    // surface ssh diagnostics: run with -v manually; check keys, agent, AllowTcpForwarding\n    logger.warn(\"ssh forward failed at startup\", { target: config.sshTarget });\n  } else throw err;\n}","preventionTips":["Verify BatchMode ssh works (key auth, no passphrase prompt) before enabling ssh exposure","Ensure remote sshd allows TCP forwarding and the remote port is free","Use StrictHostKeyChecking=accept-new and ServerAliveInterval in ssh options","Check host reachability/DNS and that outbound port 22 is not blocked"],"tags":["ssh","process-exit","network","authentication"],"backgroundTag":"ssh-exit-before-ready","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}