can1357/oh-my-pi · error

SSH control directory ${dir} ${reason}

Error message

SSH control directory ${dir} ${reason}

What it means

The SSH connection manager creates a per-host ControlMaster socket directory (e.g. under ~/.ssh/omp-control). Before using it, assertOwnerPrivateDir verifies the existing directory is actually a directory, not a symlink, owned by the current UID, and has no group/other permissions. This error is thrown when the directory exists but fails that ownership/permission guard, because a shared or symlinked control dir would let other users hijack SSH master sockets.

Source

Thrown at packages/coding-agent/src/ssh/connection-manager.ts:222

	}
	try {
		let st = fs.fstatSync(fd);
		// Normalize perms on the pinned inode only when it is ours; never fchmod a
		// directory another user owns.
		if ((uid === undefined || st.uid === uid) && (st.mode & 0o777) !== 0o700) {
			try {
				fs.fchmodSync(fd, 0o700);
				st = fs.fstatSync(fd);
			} catch (err) {
				logger.debug("SSH control dir chmod failed", { path: dir, error: String(err) });
			}
		}
		const reason = controlDirGuardError(
			{ isSymlink: false, isDir: st.isDirectory(), uid: st.uid, mode: st.mode },
			uid,
		);
		if (reason) {
			throw new Error(`SSH control directory ${dir} ${reason}`);
		}
	} finally {
		fs.closeSync(fd);
	}
}

function getHostInfoPath(name: string): string {
	return path.join(HOST_INFO_DIR, `${sanitizeHostName(name)}.json`);
}

async function deleteHostInfoFromDisk(hostName: string): Promise<void> {
	const path = getHostInfoPath(hostName);
	try {
		await fs.promises.unlink(path);
	} catch (err) {
		if (isEnoent(err)) return;
		logger.warn("Failed to delete SSH host info", { host: hostName, error: String(err) });
	}

View on GitHub (pinned to 9690622007)

Solutions

  1. Fix ownership: chown -R $(id -u):$(id -g) <control-dir> (typically ~/.ssh/omp-control)
  2. Fix permissions: chmod 700 <control-dir> and chmod 700 ~/.ssh
  3. Remove the directory if untrusted: rm -rf <control-dir> and let the tool recreate it
  4. If it is a symlink, delete it and recreate as a real directory
  5. Avoid running the CLI as root/sudo

Example fix

// before
$ ls -la ~/.ssh/omp-control
drwxr-xr-x  2 root root  ...  omp-control
// after
$ sudo chown -R $USER ~/.ssh/omp-control
$ chmod 700 ~/.ssh/omp-control
Defensive patterns

Strategy: validation

Validate before calling

import * as fs from "node:fs";
const dir = `${process.env.HOME}/.ssh/omp-control`;
try {
  const st = fs.lstatSync(dir);
  if (st.isSymbolicLink() || !st.isDirectory()) throw new Error("replace control dir");
  if (st.uid !== process.getuid?.()) throw new Error("wrong owner — chown it");
  if ((st.mode & 0o077) !== 0) throw new Error("too permissive — chmod 700");
} catch (e) {
  if (e.code !== "ENOENT") console.warn("fix control dir before connecting:", e.message);
}

Type guard

function isSafeControlDir(st: fs.Stats, uid: number): boolean {
  return st.isDirectory() && st.uid === uid && (st.mode & 0o077) === 0;
}

Try / catch

try {
  await connect(target);
} catch (err) {
  if (err instanceof Error && err.message.includes("SSH control directory")) {
    // repair: rm -rf the dir / chown / chmod 700, then retry once
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling any SSH operation (connect, runSsh, ssh:// file tools) when ~/.ssh/omp-control (or the computed control dir) already exists with wrong uid or group/other-accessible mode, or when the path was replaced by a symlink so the guard reports a reason.

Common situations: Running the CLI once with sudo (directory created as root), copying a dotfiles setup where ~/.ssh is world-readable, restoring a home directory from a backup that reset ownership, or someone symlinking the control dir to /tmp.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/1619332a53b4641f. Report an issue: GitHub.