{"record":{"id":"c91e433b9e725999","repo":"NousResearch/hermes-agent","slug":"unsafe-ssh-target-host-is-required","errorCode":null,"errorMessage":"Unsafe SSH target: host is required.","messagePattern":"Unsafe SSH target: host is required\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/desktop/electron/ssh-connection.ts","lineNumber":49,"sourceCode":"\nimport { spawn } from 'node:child_process'\nimport crypto from 'node:crypto'\nimport fs from 'node:fs'\nimport net from 'node:net'\nimport os from 'node:os'\nimport path from 'node:path'\n\nconst DEFAULT_CONNECT_TIMEOUT_MS = 15_000\nconst DEFAULT_EXEC_TIMEOUT_MS = 20_000\nconst DEFAULT_FORWARD_TIMEOUT_MS = 15_000\nconst CONTROL_PERSIST_SECONDS = 300\n\n// eslint-disable-next-line no-control-regex -- deliberately reject control chars in ssh targets\nconst _CONTROL_CHAR_RE = /[\\x00-\\x1f\\x7f]/\n\nfunction validateSshTarget(host, user, port) {\n  if (!host || typeof host !== 'string') {\n    throw new Error('Unsafe SSH target: host is required.')\n  }\n\n  if (host.startsWith('-')) {\n    throw new Error(`Unsafe SSH target: host must not start with a dash (\"${host}\").`)\n  }\n\n  if (_CONTROL_CHAR_RE.test(host)) {\n    throw new Error('Unsafe SSH target: host contains control characters.')\n  }\n\n  if (user && _CONTROL_CHAR_RE.test(user)) {\n    throw new Error('Unsafe SSH target: user contains control characters.')\n  }\n\n  if (user && user.startsWith('-')) {\n    throw new Error(`Unsafe SSH target: user must not start with a dash (\"${user}\").`)\n  }\n","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/apps/desktop/electron/ssh-connection.ts#L31-L67","documentation":"First check in validateSshTarget() in the desktop app's ssh-connection module: the SSH host must be a non-empty string. It guards SshConnection construction and target building before any value reaches an ssh command line.","triggerScenarios":"Constructing SshConnection with cfg.host undefined/null/'' or a non-string, or calling validateSshTarget directly with such a value. Typically a remote-backend config deserialized without a host field, or a connection form submitted with the host blank.","commonSituations":"Remote backend config missing the host key after a partial save; UI allowing submit with an empty host; code passing a URL where a bare hostname is expected so the field lookup returns ''; a config factory returning {} before the user has filled anything in.","solutions":["Require and validate host in the remote-backend form before attempting to connect.","Check the config source: confirm the field the code reads (host) matches the key the config writer persists.","If host comes from a URL, parse it explicitly: new URL(url).hostname."],"exampleFix":"// before\nconst conn = new SshConnection({ host: cfg.server, user: cfg.user })\n\n// after\nif (!cfg.server) throw new TypeError('remote config missing server field')\nconst conn = new SshConnection({ host: new URL(cfg.server).hostname, user: cfg.user })","handlingStrategy":"type-guard","validationCode":"if (!remoteCfg || typeof remoteCfg.host !== 'string' || remoteCfg.host.length === 0) {\n  throw new TypeError('Remote backend config must define a non-empty host string')\n}","typeGuard":"function isSshConfigWithHost(cfg: unknown): cfg is { host: string; user?: string; port?: number | string } {\n  return Boolean(cfg && typeof (cfg as any).host === 'string' && (cfg as any).host.length > 0)\n}","tryCatchPattern":"try {\n  conn = new SshConnection(cfg)\n} catch (e) {\n  if (e instanceof Error && e.message === 'SshConnection requires a host.') {\n    promptUserToCompleteRemoteConfig()\n    return\n  }\n  throw e\n}","preventionTips":["Make host a required field in the remote backend form and validate before save.","Keep the persisted field name aligned with the constructor's expected cfg.host.","Parse hosts from URLs with new URL(...).hostname rather than string splitting."],"tags":["ssh","validation","remote"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}