NousResearch/hermes-agent · error · Error

Unsafe SSH control dir: ${controlDir} is owned by uid ${st.u

Error message

Unsafe SSH control dir: ${controlDir} is owned by uid ${st.uid}, not ${process.getuid!()}.

What it means

Thrown during SSH control-master setup (POSIX only) when the control directory's owner uid does not match the current process uid (st.uid !== process.getuid()). A control dir owned by another user could let them control or read the multiplexed socket, so ownership is enforced before the master connection opens.

Source

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

    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) {
      throw this._fail(error, SSH_ERROR.UNREACHABLE)
    }

    if (result.code !== 0) {

View on GitHub (pinned to c896c09c42)

Solutions

  1. Fix ownership: sudo chown -R $(id -u):$(id -g) <controlDir>, or simply delete it so the app recreates it as the current user.
  2. Avoid running the desktop app as root/sudo.
  3. In containers, ensure consistent uids across restarts or point the control path at a per-uid directory.

Example fix

# before (shell)
sudo chown -R $(id -u):$(id -g) ~/.cache/hermes/ssh-control
# or simply
rm -rf ~/.cache/hermes/ssh-control   # app recreates it as current user

# after: control master connects normally
Defensive patterns

Strategy: validation

Validate before calling

import fs from 'node:fs'

function isControlDirOwnedByMe(dir: string): boolean {
  try {
    const st = fs.lstatSync(dir)
    return st.isDirectory() && st.uid === process.getuid!()
  } catch {
    return false
  }
}

if (!isControlDirOwnedByMe(controlDir)) {
  // refuse and instruct the user (or relocate the dir) before opening the master
}

Try / catch

try {
  await conn.open()
} catch (e) {
  if (e instanceof Error && /owned by uid/.test(e.message)) {
    showFixHint(`Run: sudo chown -R $(id -u):$(id -g) ${path.dirname(conn.controlPath)}`)
    return
  }
  throw e
}

Prevention

When it happens

Trigger: The dir was created by root (app once run with sudo), by a different local user, or by a system service — lstatSync reports a uid different from the current process uid.

Common situations: App was once launched with sudo, leaving root-owned dirs in the user's cache/runtime path; shared machines where another user pre-created the dir; containers where uid mapping differs between runs.

Related errors


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