nanocoai/nanoclaw · error · Error
cwd must be ./path, ${PLUGIN_ROOT}[/path], or ${PLUGIN_DATA}
Error message
cwd must be ./path, ${PLUGIN_ROOT}[/path], or ${PLUGIN_DATA}[/path] What it means
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.
Source
Thrown at src/container-config.ts:217
function parseStringRecord(value: unknown, flag: string): Record<string, string> | undefined {
if (value === undefined) return undefined;
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new Error(`${flag} must be a JSON object with string values`);
}
const record: Record<string, string> = {};
for (const [key, entry] of Object.entries(value)) {
if (typeof entry !== 'string') throw new Error(`${flag} must be a JSON object with string values`);
record[key] = entry;
}
return record;
}
/** Accept only the spec's fixed cwd shapes, lexically contained (no ".." segments). */
function parseCwd(value: unknown): string | undefined {
if (value === undefined) return undefined;
if (typeof value !== 'string' || !CWD_FORM_RE.test(value)) {
throw new Error('cwd must be ./path, ${PLUGIN_ROOT}[/path], or ${PLUGIN_DATA}[/path]');
}
// rest === '' is the bare form (`${PLUGIN_DATA}`, `./`); empty segments in a
// non-empty rest are rejected for symmetry with the command validator.
const rest = value.startsWith('./') ? value.slice(2) : value.replace(CWD_FORM_RE, '');
if (
rest.includes('${') ||
rest.includes('\\') ||
(rest !== '' && rest.split('/').some((s) => s === '..' || s === ''))
) {
throw new Error('cwd escapes the plugin root');
}
return value;
}
export interface AdditionalMountConfig {
hostPath: string;
containerPath: string;
readonly?: boolean;View on GitHub (pinned to 294ef2aee8)
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
Example fix
// before
{"command":"srv","cwd":"/opt/data"}
// after
{"command":"srv","cwd":"${PLUGIN_DATA}/work"} Defensive patterns
Strategy: validation
Validate before calling
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]'); Type guard
const CWD_FORM = /^(\.\/.+|\$\{PLUGIN_(ROOT|DATA)\}(\/.+)?)$/;
const isValidCwdShape = (v: unknown): v is string => typeof v === 'string' && CWD_FORM.test(v); Try / catch
catch (err) { if (err.message.includes('cwd must be ./path')) reformatCwd(); else throw err; } Prevention
- Use only the three sanctioned cwd shapes
- Never absolute paths or other ${VARS}
When it happens
Trigger: cwd values like "/srv/app", "../up", "${HOME}/x", "src/tools" (no ./ prefix), or a non-string value in a stdio MCP entry.
Common situations: Using an absolute host path out of habit; referencing other template variables; forgetting the leading ./ for relative paths.
Related errors
- type "stdio" requires command
- type "http" requires url
- MCP instructions must be a string
- Provide exactly one of command or url
- args, env, and cwd are only valid with command
AI-assisted analysis of nanocoai/nanoclaw@294ef2aee8 (2026-08-28).
Data as JSON: /api/errors/306a9b868dc5bb44.
Report an issue: GitHub.