{"record":{"id":"f15c1b159f769b6e","repo":"NousResearch/hermes-agent","slug":"ssh-host-is-required","errorCode":null,"errorMessage":"SSH host is required.","messagePattern":"SSH host is required\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/desktop/electron/main.ts","lineNumber":7144,"sourceCode":"// Build an SSH connection block from a save payload, preserving an\n// already-adopted dashboard token from the existing block (the token is minted\n// + reconciled at bootstrap, never user-entered). `mode: 'ssh'` is stamped so\n// normalizeSshConfig/profileSshOverride recognize it.\nfunction buildSshBlock(input: any, existingBlock: any = {}) {\n  // `??` (not `||`) so an explicit '' (user CLEARED the field) wins over the\n  // saved value; only a truly absent (undefined) field inherits.\n  const merged = normalizeSshConfig({\n    mode: 'ssh',\n    host: input.sshHost ?? existingBlock.host,\n    user: input.sshUser ?? existingBlock.user,\n    port: input.sshPort ?? existingBlock.port,\n    keyPath: input.sshKeyPath ?? existingBlock.keyPath,\n    remoteHermesPath: input.sshRemoteHermesPath ?? existingBlock.remoteHermesPath,\n    remoteProfile: input.sshRemoteProfile ?? existingBlock.remoteProfile\n  })\n\n  if (!merged) {\n    throw new Error('SSH host is required.')\n  }\n\n  // Carry forward an already-adopted dashboard token unless the host changed\n  // (a different host invalidates the old dashboard's token).\n  if (existingBlock.token && existingBlock.host === merged.host) {\n    merged.token = existingBlock.token\n  }\n\n  return merged\n}\n\n// Build a remote backend connection descriptor from an already-resolved remote\n// config. Handles both auth models (OAuth ws-ticket vs static session token)\n// and is shared by the per-profile, env, and global resolution paths. `token`\n// is the DECRYPTED static token (or null in OAuth mode). `source` is a label\n// for diagnostics ('profile' | 'env' | 'settings').\nasync function buildRemoteConnection(\n  rawUrl,","sourceCodeStart":7126,"sourceCodeEnd":7162,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/apps/desktop/electron/main.ts#L7126-L7162","documentation":"Thrown after normalizeSshConfig returns null when merging SSH connection settings. normalizeSshConfig yields null when the required host field is empty — here the merge of input.sshHost over the existing block's host (undefined input inherits the saved value) still produced no host. Note the merge semantics: only a truly absent (undefined) input field inherits, so an empty-string input overrides a saved host with ''.","triggerScenarios":"Saving an SSH remote with no host in either the new input or the existing block; passing sshHost: '' (which, being defined, does NOT inherit the saved host) — the classic footgun called out in the comment.","commonSituations":"Form serialization sending '' for untouched fields instead of omitting them; clearing the host field and saving; first-time SSH config with all fields blank.","solutions":["Provide a non-empty sshHost.","In calling code, omit fields the user didn't fill (send undefined, not '') so the existing saved values inherit.","Strip empty strings before merging: input.sshHost || undefined."],"exampleFix":"// before — '' overrides the saved host and normalizeSshConfig returns null\nconst merged = normalizeSshConfig({ mode: 'ssh', host: input.sshHost ?? existingBlock.host, ... })\n\n// after — coerce empty strings to undefined so saved values inherit\nconst orUndef = (v) => (v === '' ? undefined : v)\nconst merged = normalizeSshConfig({\n  mode: 'ssh',\n  host: orUndef(input.sshHost) ?? existingBlock.host,\n  user: orUndef(input.sshUser) ?? existingBlock.user,\n  port: orUndef(input.sshPort) ?? existingBlock.port\n})","handlingStrategy":"validation","validationCode":"const orUndef = (v) => (v === '' || v == null ? undefined : v)\nfunction sshMergeReady(input, existing) {\n  const host = orUndef(input.sshHost) ?? existing?.host\n  return typeof host === 'string' && host.trim() !== ''\n}","typeGuard":"function isNonEmptyHost(host) {\n  return typeof host === 'string' && host.trim() !== ''\n}","tryCatchPattern":null,"preventionTips":["Send undefined (omit), never '', for unfilled SSH form fields","Remember the merge semantics: defined-but-empty overrides the saved value","Validate host presence client-side before save"],"tags":["ssh","remote","validation","config","desktop"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}