{"record":{"id":"a8ac9aec86f9666b","repo":"can1357/oh-my-pi","slug":"ssh-key-permissions-must-be-600-or-stricter-key","errorCode":null,"errorMessage":"SSH key permissions must be 600 or stricter: ${keyPath}","messagePattern":"SSH key permissions must be 600 or stricter: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/ssh/connection-manager.ts","lineNumber":260,"sourceCode":"\nasync function validateKeyPermissions(keyPath?: string, platform: SshPlatform = process.platform): Promise<void> {\n\tif (!keyPath) return;\n\tlet stats: fs.Stats;\n\ttry {\n\t\tstats = await fs.promises.stat(keyPath);\n\t} catch (err) {\n\t\tif (isEnoent(err)) {\n\t\t\tthrow new Error(`SSH key not found: ${keyPath}`);\n\t\t}\n\t\tthrow err;\n\t}\n\tif (!stats.isFile()) {\n\t\tthrow new Error(`SSH key is not a file: ${keyPath}`);\n\t}\n\tif (platform === \"win32\") return;\n\tconst mode = stats.mode & 0o777;\n\tif ((mode & 0o077) !== 0) {\n\t\tthrow new Error(`SSH key permissions must be 600 or stricter: ${keyPath}`);\n\t}\n}\n\nfunction buildCommonArgs(host: SSHConnectionTarget, options?: SSHArgsOptions): string[] {\n\tconst args = options?.allowStdin ? [] : [\"-n\"];\n\n\tif (supportsSshControlMaster(options?.platform)) {\n\t\targs.push(\"-o\", \"ControlMaster=auto\", \"-o\", `ControlPath=${CONTROL_PATH}`, \"-o\", \"ControlPersist=3600\");\n\t}\n\n\targs.push(\"-o\", \"BatchMode=yes\", \"-o\", \"StrictHostKeyChecking=accept-new\");\n\n\tif (host.port) {\n\t\targs.push(\"-p\", String(host.port));\n\t}\n\tif (host.keyPath) {\n\t\targs.push(\"-i\", host.keyPath);\n\t}","sourceCodeStart":242,"sourceCodeEnd":278,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/ssh/connection-manager.ts#L242-L278","documentation":"On non-Windows platforms, validateKeyPermissions enforces that the private key has no group/other permission bits ((mode & 0o077) === 0), i.e. 600 or stricter. SSH itself refuses world/group-readable private keys; this library fails fast with a clear message before spawning ssh.","triggerScenarios":"Connecting with a key whose file mode is e.g. 644, 664, or 755 on Linux/macOS; commonly after copying a key with scp -r, downloading it via a browser, or cloning it from git (git preserves only the executable bit, yielding 644).","commonSituations":"Fresh key copied from Windows/macOS to Linux, key checked into a repo and checked out with 644, WSL accessing a key under /mnt/c (drvfs mounts are 777), backup-restore that reset modes.","solutions":["chmod 600 <keyPath>","If under WSL /mnt/c, move the key into the Linux filesystem (~/) and chmod 600, or remount with correct metadata","Avoid storing keys in git; if you must, fix the mode after checkout"],"exampleFix":"// before\n$ ls -l ~/.ssh/id_ed25519\n-rw-r--r-- 1 me me ... id_ed25519\n// after\n$ chmod 600 ~/.ssh/id_ed25519","handlingStrategy":"validation","validationCode":"import * as fs from \"node:fs/promises\";\nif (process.platform !== \"win32\") {\n  const st = await fs.stat(keyPath);\n  if ((st.mode & 0o077) !== 0) {\n    throw new Error(`chmod 600 ${keyPath} before connecting (mode ${(st.mode & 0o777).toString(8)})`);\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  await connect(target);\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"permissions must be 600\")) {\n    const p = err.message.split(\": \").pop()!;\n    await $`chmod 600 ${p}`.quiet().nothrow();\n    return connect(target); // retry once after fixing\n  }\n  throw err;\n}","preventionTips":["Always chmod 600 keys after creating/copying them","On WSL keep keys under the Linux FS (~/), not /mnt/c","Don't commit private keys to git — checkout resets modes to 644","Add a preflight check in deploy scripts: stat -c '%a' key"],"tags":["ssh","permissions","security","filesystem"],"backgroundTag":"ssh-key-permissions-too-open","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}