NousResearch/hermes-agent · error · Error

SSH host is required.

Error message

SSH host is required.

What it means

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 ''.

Source

Thrown at apps/desktop/electron/main.ts:7144

// Build an SSH connection block from a save payload, preserving an
// already-adopted dashboard token from the existing block (the token is minted
// + reconciled at bootstrap, never user-entered). `mode: 'ssh'` is stamped so
// normalizeSshConfig/profileSshOverride recognize it.
function buildSshBlock(input: any, existingBlock: any = {}) {
  // `??` (not `||`) so an explicit '' (user CLEARED the field) wins over the
  // saved value; only a truly absent (undefined) field inherits.
  const merged = normalizeSshConfig({
    mode: 'ssh',
    host: input.sshHost ?? existingBlock.host,
    user: input.sshUser ?? existingBlock.user,
    port: input.sshPort ?? existingBlock.port,
    keyPath: input.sshKeyPath ?? existingBlock.keyPath,
    remoteHermesPath: input.sshRemoteHermesPath ?? existingBlock.remoteHermesPath,
    remoteProfile: input.sshRemoteProfile ?? existingBlock.remoteProfile
  })

  if (!merged) {
    throw new Error('SSH host is required.')
  }

  // Carry forward an already-adopted dashboard token unless the host changed
  // (a different host invalidates the old dashboard's token).
  if (existingBlock.token && existingBlock.host === merged.host) {
    merged.token = existingBlock.token
  }

  return merged
}

// Build a remote backend connection descriptor from an already-resolved remote
// config. Handles both auth models (OAuth ws-ticket vs static session token)
// and is shared by the per-profile, env, and global resolution paths. `token`
// is the DECRYPTED static token (or null in OAuth mode). `source` is a label
// for diagnostics ('profile' | 'env' | 'settings').
async function buildRemoteConnection(
  rawUrl,

View on GitHub (pinned to c896c09c42)

Solutions

  1. Provide a non-empty sshHost.
  2. In calling code, omit fields the user didn't fill (send undefined, not '') so the existing saved values inherit.
  3. Strip empty strings before merging: input.sshHost || undefined.

Example fix

// before — '' overrides the saved host and normalizeSshConfig returns null
const merged = normalizeSshConfig({ mode: 'ssh', host: input.sshHost ?? existingBlock.host, ... })

// after — coerce empty strings to undefined so saved values inherit
const orUndef = (v) => (v === '' ? undefined : v)
const merged = normalizeSshConfig({
  mode: 'ssh',
  host: orUndef(input.sshHost) ?? existingBlock.host,
  user: orUndef(input.sshUser) ?? existingBlock.user,
  port: orUndef(input.sshPort) ?? existingBlock.port
})
Defensive patterns

Strategy: validation

Validate before calling

const orUndef = (v) => (v === '' || v == null ? undefined : v)
function sshMergeReady(input, existing) {
  const host = orUndef(input.sshHost) ?? existing?.host
  return typeof host === 'string' && host.trim() !== ''
}

Type guard

function isNonEmptyHost(host) {
  return typeof host === 'string' && host.trim() !== ''
}

Prevention

When it happens

Trigger: 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.

Common situations: Form serialization sending '' for untouched fields instead of omitting them; clearing the host field and saving; first-time SSH config with all fields blank.

Related errors


AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14). Data as JSON: /api/errors/f15c1b159f769b6e. Report an issue: GitHub.