NousResearch/hermes-agent · error · Error

Unsafe SSH control dir: ${controlDir} is not a directory.

Error message

Unsafe SSH control dir: ${controlDir} is not a directory.

What it means

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.

Source

Thrown at apps/desktop/electron/ssh-connection.ts:592

    }

    const controlDir = path.dirname(this.controlPath)

    try {
      fs.mkdirSync(controlDir, { recursive: true, mode: 0o700 })
    } catch {
      void 0
    }

    if (process.platform !== 'win32') {
      const st = fs.lstatSync(controlDir)

      if (st.isSymbolicLink()) {
        throw new Error(`Unsafe SSH control dir: ${controlDir} is a symlink.`)
      }

      if (!st.isDirectory()) {
        throw new Error(`Unsafe SSH control dir: ${controlDir} is not a directory.`)
      }

      if (st.uid !== process.getuid!()) {
        throw new Error(`Unsafe SSH control dir: ${controlDir} is owned by uid ${st.uid}, not ${process.getuid!()}.`)
      }

      if ((st.mode & 0o777) !== 0o700) {
        fs.chmodSync(controlDir, 0o700)
      }
    }

    const args = buildMasterArgs(this, this._connectTimeoutMs)
    this._logLine(`opening control master to ${target(this.user, this.host)}:${this.port}`)
    let result

    try {
      result = await runSsh(args, { timeoutMs: this._connectTimeoutMs, spawnFn: this._spawnFn })
    } catch (error) {

View on GitHub (pinned to c896c09c42)

Solutions

  1. Remove the offending non-directory entry and recreate it as a directory with mode 0700.
  2. Run mkdir -p on the path manually to surface the real error the swallowed catch hid (permissions, mount point).
  3. Move the control path to a clean app-owned directory.

Example fix

# before (shell)
ls -la ~/.cache/hermes/ssh-control   # regular file

# after
rm ~/.cache/hermes/ssh-control && mkdir -m 700 ~/.cache/hermes/ssh-control
Defensive patterns

Strategy: validation

Validate before calling

import fs from 'node:fs'

function ensureControlDir(dir: string): void {
  if (fs.existsSync(dir)) {
    const st = fs.lstatSync(dir)
    if (!st.isDirectory()) fs.rmSync(dir) // clear stray non-dir entry, recreate below
  }
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
}

Try / catch

try {
  await conn.open()
} catch (e) {
  if (e instanceof Error && /is not a directory/.test(e.message)) {
    fs.rmSync(path.dirname(conn.controlPath))
    await conn.open() // app recreates the dir with 0700 on retry
  } else throw e
}

Prevention

When it happens

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

Common situations: 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.

Related errors


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