{"record":{"id":"9c51413ef01f8adf","repo":"NousResearch/hermes-agent","slug":"unsafe-ssh-control-dir-controldir-is-not-a-dir","errorCode":null,"errorMessage":"Unsafe SSH control dir: ${controlDir} is not a directory.","messagePattern":"Unsafe SSH control dir: (.+?) is not a directory\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/desktop/electron/ssh-connection.ts","lineNumber":592,"sourceCode":"    }\n\n    const controlDir = path.dirname(this.controlPath)\n\n    try {\n      fs.mkdirSync(controlDir, { recursive: true, mode: 0o700 })\n    } catch {\n      void 0\n    }\n\n    if (process.platform !== 'win32') {\n      const st = fs.lstatSync(controlDir)\n\n      if (st.isSymbolicLink()) {\n        throw new Error(`Unsafe SSH control dir: ${controlDir} is a symlink.`)\n      }\n\n      if (!st.isDirectory()) {\n        throw new Error(`Unsafe SSH control dir: ${controlDir} is not a directory.`)\n      }\n\n      if (st.uid !== process.getuid!()) {\n        throw new Error(`Unsafe SSH control dir: ${controlDir} is owned by uid ${st.uid}, not ${process.getuid!()}.`)\n      }\n\n      if ((st.mode & 0o777) !== 0o700) {\n        fs.chmodSync(controlDir, 0o700)\n      }\n    }\n\n    const args = buildMasterArgs(this, this._connectTimeoutMs)\n    this._logLine(`opening control master to ${target(this.user, this.host)}:${this.port}`)\n    let result\n\n    try {\n      result = await runSsh(args, { timeoutMs: this._connectTimeoutMs, spawnFn: this._spawnFn })\n    } catch (error) {","sourceCodeStart":574,"sourceCodeEnd":610,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/apps/desktop/electron/ssh-connection.ts#L574-L610","documentation":"Thrown during SSH control-master setup (POSIX only) when lstat() shows the control-socket directory exists but is not a directory (a regular file, socket, device node, ...). The control socket cannot live inside a non-directory, and an unexpected file type there signals a broken or tampered state.","triggerScenarios":"path.dirname(controlPath) exists as a plain file — a placeholder file, log, or editor backup occupies the exact path, or the preceding mkdirSync failed (its error is swallowed by the catch) because the non-dir entry already existed.","commonSituations":"A file created at the exact path where the control dir should be; mkdir failing silently on EEXIST-with-wrong-type; partial cleanup from an old layout leaving a stray file.","solutions":["Remove the offending non-directory entry and recreate it as a directory with mode 0700.","Run mkdir -p on the path manually to surface the real error the swallowed catch hid (permissions, mount point).","Move the control path to a clean app-owned directory."],"exampleFix":"# before (shell)\nls -la ~/.cache/hermes/ssh-control   # regular file\n\n# after\nrm ~/.cache/hermes/ssh-control && mkdir -m 700 ~/.cache/hermes/ssh-control","handlingStrategy":"validation","validationCode":"import fs from 'node:fs'\n\nfunction ensureControlDir(dir: string): void {\n  if (fs.existsSync(dir)) {\n    const st = fs.lstatSync(dir)\n    if (!st.isDirectory()) fs.rmSync(dir) // clear stray non-dir entry, recreate below\n  }\n  fs.mkdirSync(dir, { recursive: true, mode: 0o700 })\n}","typeGuard":null,"tryCatchPattern":"try {\n  await conn.open()\n} catch (e) {\n  if (e instanceof Error && /is not a directory/.test(e.message)) {\n    fs.rmSync(path.dirname(conn.controlPath))\n    await conn.open() // app recreates the dir with 0700 on retry\n  } else throw e\n}","preventionTips":["Create the control dir with mkdirSync(recursive, 0700) at app startup, before any connection.","Do not pre-create placeholder files at the control dir path.","Log mkdirSync failures instead of swallowing them, so a blocked creation is visible."],"tags":["ssh","security","filesystem"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}