{"record":{"id":"d7698489830587b5","repo":"paperclipai/paperclip","slug":"private-state-directory-is-not-a-real-directory","errorCode":null,"errorMessage":"Private state directory is not a real directory: ${diagnosticsDirectory}","messagePattern":"Private state directory is not a real directory: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/control-plane/durable-prp-control-plane.ts","lineNumber":3409,"sourceCode":"  const withRestart = (handle: RunnerProcessHandle): RunnerProcessHandle => ({\n    ...handle,\n    restart: (ticket) => spawnRunner({ ...options, ticket }),\n  });\n  if (options.processLauncher !== undefined) {\n    return withRestart(\n      options.processLauncher({ command, args, cwd: packageRoot, environment }),\n    );\n  }\n\n  const detached = process.platform !== \"win32\";\n  const diagnosticsDirectory = options.diagnosticsDirectory;\n  let stdoutPath: string | null = null;\n  let stderrPath: string | null = null;\n  if (diagnosticsDirectory) {\n    try {\n      const metadata = lstatSync(diagnosticsDirectory);\n      if (metadata.isSymbolicLink() || !metadata.isDirectory()) {\n        throw new Error(\n          `Private state directory is not a real directory: ${diagnosticsDirectory}`,\n        );\n      }\n    } catch (error) {\n      if (!isNodeError(error, \"ENOENT\")) throw error;\n      mkdirSync(diagnosticsDirectory, { recursive: true, mode: 0o700 });\n    }\n    if (process.platform !== \"win32\") chmodSync(diagnosticsDirectory, 0o700);\n    verifyPrivateDirectory(diagnosticsDirectory);\n    stdoutPath = resolve(diagnosticsDirectory, \"runnerd.stdout.log\");\n    stderrPath = resolve(diagnosticsDirectory, \"runnerd.stderr.log\");\n    // runnerd owns every durable diagnostic write so it can redact and bound\n    // the complete value before a byte reaches disk. Raw process output is\n    // intentionally discarded below; these files are only the runner-owned\n    // restart-survivable diagnostic channel.\n    atomicPrivateWrite(stdoutPath, \"\");\n    atomicPrivateWrite(stderrPath, \"\");\n  }","sourceCodeStart":3391,"sourceCodeEnd":3427,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/control-plane/durable-prp-control-plane.ts#L3391-L3427","documentation":"When a diagnostics directory is configured, the launcher verifies with lstatSync that the path is a real directory and not a symbolic link, because the directory holds private state requiring 0o700 semantics. If lstat reports a symlink or non-directory, the error is thrown directly; other errors propagate except ENOENT, which is handled by creating the directory recursively with mode 0o700.","triggerScenarios":"Setting diagnosticsDirectory to a symlink pointing at a directory, to a regular file, or to any non-directory node. Note the throw is also caught by the surrounding try and re-thrown unless it is ENOENT, so this error always surfaces to the caller.","commonSituations":"Pointing the diagnostics dir at /tmp-style symlinked paths (e.g. macOS /var -> /private/var), pre-creating the path as a file, or using a symlink farm for state directories.","solutions":["Replace the symlink or file at diagnosticsDirectory with a real directory (mkdir -p, then use the resolved path).","Use fs.realpathSync on the target and pass the resolved real path instead of the symlink.","Remove any pre-created file at that path and let the launcher create the directory itself (it mkdirs with mode 0o700 on ENOENT).","Validate the path in your own setup code before launching."],"exampleFix":"// before\ndiagnosticsDirectory: \"/var/lib/runner-diagnostics\" // symlink to /private/var/...\n// after\ndiagnosticsDirectory: fs.realpathSync(\"/var/lib/runner-diagnostics\"); // real directory, no symlink","handlingStrategy":"validation","validationCode":"import { lstatSync } from \"node:fs\";\nfunction isRealDirectory(p) {\n  try {\n    const st = lstatSync(p);\n    return !st.isSymbolicLink() && st.isDirectory();\n  } catch { return false; }\n}\nif (diagnosticsDirectory && !isRealDirectory(diagnosticsDirectory)) {\n  throw new Error(`not a real directory: ${diagnosticsDirectory}`);\n}","typeGuard":"function isRealDirectoryPath(p) {\n  try {\n    const st = lstatSync(p);\n    return st.isDirectory() && !st.isSymbolicLink();\n  } catch { return false; }\n}","tryCatchPattern":"try {\n  startRunner({ diagnosticsDirectory });\n} catch (err) {\n  if (err.message.startsWith(\"Private state directory\")) {\n    // replace symlink/file with a real directory and retry\n  } else throw err;\n}","preventionTips":["Never place symlinks on private-state paths; resolve with realpathSync first.","Let the launcher create the directory (it mkdirs 0o700) instead of pre-creating odd nodes.","Check the path with lstat (not stat) so symlinks are caught."],"tags":["filesystem","security","configuration"],"backgroundTag":"path-is-not-a-directory","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}