{"record":{"id":"8825db8f16ab1ff0","repo":"NousResearch/hermes-agent","slug":"not-a-directory-watchdir","errorCode":null,"errorMessage":"Not a directory: ${watchDir}","messagePattern":"Not a directory: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/desktop/electron/main.ts","lineNumber":4964,"sourceCode":"  return true\n}\n\nfunction closePreviewWatchers() {\n  for (const id of previewWatchers.keys()) {\n    stopPreviewFileWatch(id)\n  }\n}\n\n/** Watch a DIRECTORY for entry churn (folders appearing/vanishing) — the\n *  disk-plugin door's \"new plugin folder\" signal, replacing the renderer's 5s\n *  readdir poll. Same registry + change channel as the preview file watchers\n *  (the renderer reconciles on any tick; per-file edits stay on their own\n *  watches), so stopPreviewFileWatch/closePreviewWatchers manage these too. */\nfunction watchDirectory(rawDir) {\n  const watchDir = path.resolve(String(rawDir || ''))\n\n  if (!fs.existsSync(watchDir) || !fs.statSync(watchDir).isDirectory()) {\n    throw new Error(`Not a directory: ${watchDir}`)\n  }\n\n  const id = crypto.randomBytes(12).toString('base64url')\n  let timer = null\n\n  const watcher = fs.watch(watchDir, () => {\n    if (timer) {\n      clearTimeout(timer)\n    }\n\n    timer = setTimeout(() => {\n      timer = null\n      sendPreviewFileChanged({ id, path: watchDir, url: pathToFileURL(watchDir).toString() })\n    }, PREVIEW_WATCH_DEBOUNCE_MS)\n  })\n\n  previewWatchers.set(id, {\n    close: () => {","sourceCodeStart":4946,"sourceCodeEnd":4982,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/apps/desktop/electron/main.ts#L4946-L4982","documentation":"Thrown by watchDirectory, the fs.watch-based watcher that replaced the disk-plugin door's 5s readdir poll. It requires the target to be an existing directory — fs.existsSync is false, or statSync shows a non-directory (a file was passed). Used for the plugins directory churn signal; the renderer reconciles on watcher ticks.","triggerScenarios":"IPC watch request for a plugins directory that doesn't exist yet (fresh install, plugins never created); the path points at a file; the directory was deleted between the renderer listing it and the watch call.","commonSituations":"First run before ~/.hermes/plugins is created; a plugins folder removed while the disk-plugin door was open; passing a file path by mistake.","solutions":["Create the directory first: fs.mkdirSync(watchDir, { recursive: true }) before starting the watch.","Pass the plugins directory path, not a file inside it.","Re-request the watch when the directory is (re)created instead of caching a failure."],"exampleFix":"// before\nwatchDirectory(pluginDir) // throws if dir absent\n\n// after\nif (!fs.existsSync(pluginDir)) fs.mkdirSync(pluginDir, { recursive: true })\nwatchDirectory(pluginDir)","handlingStrategy":"validation","validationCode":"const fs = require('fs')\nfunction ensureWatchableDir(dir) {\n  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })\n  return fs.statSync(dir).isDirectory()\n}","typeGuard":"function isExistingDirectory(p) {\n  try {\n    return fs.statSync(p).isDirectory()\n  } catch {\n    return false\n  }\n}","tryCatchPattern":null,"preventionTips":["mkdir -p the plugins dir before watching","Handle ENOENT from fs.watch after the dir vanishes","Re-request the watch when the directory is recreated"],"tags":["fs-watch","plugins","filesystem","validation","desktop"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}