{"record":{"id":"306a9b868dc5bb44","repo":"nanocoai/nanoclaw","slug":"cwd-must-be-path-plugin-root-path-or-pl","errorCode":null,"errorMessage":"cwd must be ./path, ${PLUGIN_ROOT}[/path], or ${PLUGIN_DATA}[/path]","messagePattern":"cwd must be \\./path, (.+?)\\[/path\\], or (.+?)\\[/path\\]","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/container-config.ts","lineNumber":217,"sourceCode":"\nfunction parseStringRecord(value: unknown, flag: string): Record<string, string> | undefined {\n  if (value === undefined) return undefined;\n  if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n    throw new Error(`${flag} must be a JSON object with string values`);\n  }\n  const record: Record<string, string> = {};\n  for (const [key, entry] of Object.entries(value)) {\n    if (typeof entry !== 'string') throw new Error(`${flag} must be a JSON object with string values`);\n    record[key] = entry;\n  }\n  return record;\n}\n\n/** Accept only the spec's fixed cwd shapes, lexically contained (no \"..\" segments). */\nfunction parseCwd(value: unknown): string | undefined {\n  if (value === undefined) return undefined;\n  if (typeof value !== 'string' || !CWD_FORM_RE.test(value)) {\n    throw new Error('cwd must be ./path, ${PLUGIN_ROOT}[/path], or ${PLUGIN_DATA}[/path]');\n  }\n  // rest === '' is the bare form (`${PLUGIN_DATA}`, `./`); empty segments in a\n  // non-empty rest are rejected for symmetry with the command validator.\n  const rest = value.startsWith('./') ? value.slice(2) : value.replace(CWD_FORM_RE, '');\n  if (\n    rest.includes('${') ||\n    rest.includes('\\\\') ||\n    (rest !== '' && rest.split('/').some((s) => s === '..' || s === ''))\n  ) {\n    throw new Error('cwd escapes the plugin root');\n  }\n  return value;\n}\n\nexport interface AdditionalMountConfig {\n  hostPath: string;\n  containerPath: string;\n  readonly?: boolean;","sourceCodeStart":199,"sourceCodeEnd":235,"githubUrl":"https://github.com/nanocoai/nanoclaw/blob/294ef2aee85218b23ad30eda9dfe10e590b54a8c/src/container-config.ts#L199-L235","documentation":"parseCwd accepts only three fixed shapes for a stdio MCP server's working directory: `./path`, `${PLUGIN_ROOT}[/path]`, or `${PLUGIN_DATA}[/path]` — literally those placeholder strings, lexically checked by CWD_FORM_RE. Anything else (absolute paths, `..`, other variables) fails this first check.","triggerScenarios":"cwd values like \"/srv/app\", \"../up\", \"${HOME}/x\", \"src/tools\" (no ./ prefix), or a non-string value in a stdio MCP entry.","commonSituations":"Using an absolute host path out of habit; referencing other template variables; forgetting the leading ./ for relative paths.","solutions":["Use ./relative/path for plugin-relative dirs","Use the literal ${PLUGIN_ROOT} or ${PLUGIN_DATA} placeholder plus an optional /sub/path","Omit cwd entirely if the default working directory is fine"],"exampleFix":"// before\n{\"command\":\"srv\",\"cwd\":\"/opt/data\"}\n// after\n{\"command\":\"srv\",\"cwd\":\"${PLUGIN_DATA}/work\"}","handlingStrategy":"validation","validationCode":"if (entry.cwd !== undefined && !/^(\\.\\/[^]*|\\$\\{PLUGIN_ROOT\\}(\\/.*)?|\\$\\{PLUGIN_DATA\\}(\\/.*)?)$/.test(entry.cwd)) throw new UserError('cwd must be ./path, ${PLUGIN_ROOT}[/path], or ${PLUGIN_DATA}[/path]');","typeGuard":"const CWD_FORM = /^(\\.\\/.+|\\$\\{PLUGIN_(ROOT|DATA)\\}(\\/.+)?)$/;\nconst isValidCwdShape = (v: unknown): v is string => typeof v === 'string' && CWD_FORM.test(v);","tryCatchPattern":"catch (err) { if (err.message.includes('cwd must be ./path')) reformatCwd(); else throw err; }","preventionTips":["Use only the three sanctioned cwd shapes","Never absolute paths or other ${VARS}"],"tags":["mcp","config-validation","path-validation"],"backgroundTag":"invalid-path-format","analyzedSha":"294ef2aee85218b23ad30eda9dfe10e590b54a8c","analyzedAt":"2026-08-28T13:59:10.357Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}