{"record":{"id":"4699647c4c635f4b","repo":"NousResearch/hermes-agent","slug":"unsafe-ssh-key-path-contains-control-characters","errorCode":null,"errorMessage":"Unsafe SSH key path: contains control characters.","messagePattern":"Unsafe SSH key path: contains control characters\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/desktop/electron/ssh-connection.ts","lineNumber":81,"sourceCode":"\n  if (user && user.startsWith('-')) {\n    throw new Error(`Unsafe SSH target: user must not start with a dash (\"${user}\").`)\n  }\n\n  const p = Number(port)\n\n  if (!Number.isInteger(p) || p < 1 || p > 65535) {\n    throw new Error(`Unsafe SSH port: ${port} (must be 1-65535).`)\n  }\n}\n\nfunction validateKeyPath(keyPath) {\n  if (!keyPath) {\n    return\n  }\n\n  if (_CONTROL_CHAR_RE.test(keyPath)) {\n    throw new Error('Unsafe SSH key path: contains control characters.')\n  }\n\n  if (keyPath.startsWith('-')) {\n    throw new Error(`Unsafe SSH key path: must not start with a dash (\"${keyPath}\").`)\n  }\n}\n\n// Token / secret redaction\n\nconst _REDACTIONS: Array<[RegExp, string]> = [\n  [/(HERMES_DASHBOARD_SESSION_TOKEN=)(\\S+)/g, '$1<redacted>'],\n  [/(X-Hermes-Session-Token[\"']?\\s*[:=]\\s*[\"']?)([^\\s\"'&]+)/gi, '$1<redacted>'],\n  [/(Authorization[\"']?\\s*:\\s*Bearer\\s+)(\\S+)/gi, '$1<redacted>'],\n  [/([?&](?:token|ticket)=)([^\\s&\"']+)/gi, '$1<redacted>']\n]\n\nfunction redactSecrets(text) {\n  let out = String(text == null ? '' : text)","sourceCodeStart":63,"sourceCodeEnd":99,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/apps/desktop/electron/ssh-connection.ts#L63-L99","documentation":"Thrown by validateKeyPath() in ssh-connection when a truthy SSH key path matches _CONTROL_CHAR_RE (/[\\x00-\\x1f\\x7f]/). The key path becomes an ssh -i argument, so control bytes are rejected as a command-corruption and injection hazard. An empty/falsy keyPath is allowed (validation is skipped).","triggerScenarios":"cfg.keyPath containing tab, newline, escape, NUL, or DEL — pasted paths with invisible characters, config corruption, or a path stored with a trailing carriage return.","commonSituations":"Path pasted from a terminal that included a line-wrap escape sequence; CRLF line endings in a config file leaving \\r at the end of the stored value.","solutions":["Sanitize the path: strip /[\\x00-\\x1f\\x7f]/ and trim before passing it in.","Re-type the path manually; hex-inspect the stored config to locate the offending byte.","Normalize config line endings to LF so \\r never lands inside stored values."],"exampleFix":"// before\nnew SshConnection({ host, keyPath: rawKeyPath })\n\n// after\nconst keyPath = rawKeyPath.replace(/[\\x00-\\x1f\\x7f]/g, '').trim()\nnew SshConnection({ host, keyPath })","handlingStrategy":"validation","validationCode":"const CONTROL = /[\\x00-\\x1f\\x7f]/\nif (keyPath && (typeof keyPath !== 'string' || CONTROL.test(keyPath))) {\n  rejectConfig('SSH key path contains control characters')\n}","typeGuard":"function isControlFreeKeyPath(k: unknown): k is string {\n  return typeof k === 'string' && !/[\\x00-\\x1f\\x7f]/.test(k)\n}","tryCatchPattern":"try {\n  validateKeyPath(keyPath)\n} catch (e) {\n  if (e instanceof Error && e.message === 'Unsafe SSH key path: contains control characters.') {\n    keyPath = keyPath.replace(/[\\x00-\\x1f\\x7f]/g, '').trim()\n    validateKeyPath(keyPath)\n  } else throw e\n}","preventionTips":["Store key paths with LF-only line endings in config files to avoid stray \\r.","Re-type or sanitize pasted paths; terminal escape sequences are invisible in the UI.","Prefer absolute key paths chosen via a file picker over free-text entry."],"tags":["ssh","security","validation","path"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}