nanocoai/nanoclaw · error · Error

cwd escapes the plugin root

Error message

cwd escapes the plugin root

What it means

parseCwd's containment check (container-config.ts:227): after matching the allowed prefix shape, the remaining path contains `..` segments, `${`, a backslash, or empty segments (e.g. "a//b") — i.e. it could escape the plugin root. This is a lexical security guard, not a filesystem check.

Source

Thrown at src/container-config.ts:227

  }
  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;
}

/** Shape of the materialized `container.json` file read by the container runner. */
export interface ContainerConfig {
  mcpServers: Record<string, McpServerConfig>;
  packages: { apt: string[]; npm: string[] };
  imageTag?: string;
  additionalMounts: AdditionalMountConfig[];
  skills: string[] | 'all';
  provider?: string;

View on GitHub (pinned to 294ef2aee8)

Solutions

  1. Remove `..` segments; stay within the plugin root/data dir
  2. Use forward slashes only (no backslashes)
  3. Build subpaths by appending clean segments, then sanity-check with a regex before saving

Example fix

// before
{"cwd":"${PLUGIN_ROOT}/../shared"}
// after
{"cwd":"${PLUGIN_ROOT}/shared-links"}
Defensive patterns

Strategy: validation

Validate before calling

const rest = cwd.replace(/^\.\//, '').replace(/^\$\{PLUGIN_(ROOT|DATA)\}/, '');
if (rest.includes('..') || rest.includes('\\') || rest.includes('${') || rest.split('/').some(s => s === '')) throw new UserError('cwd escapes plugin root');

Type guard

const isContainedCwd = (v: string) => { const r = v.replace(/^\.\//, '').replace(/^\$\{PLUGIN_(ROOT|DATA)\}/, ''); return !r.includes('..') && !r.includes('\\') && !r.includes('${') && (r === '' || r.split('/').every(s => s !== '')); };

Try / catch

catch (err) { if (err.message.includes('escapes the plugin root')) rebuildPathWithoutTraversal(); else throw err; }

Prevention

When it happens

Trigger: cwd values like "./a/../../etc", "${PLUGIN_ROOT}/../x", "./a\\b", or "./a//b" in a stdio MCP entry.

Common situations: Path traversal attempts or careless ../ joins in generated configs; Windows-style separators; double slashes from naive path concatenation.

Related errors


AI-assisted analysis of nanocoai/nanoclaw@294ef2aee8 (2026-08-28). Data as JSON: /api/errors/b6e05a4b59baa18d. Report an issue: GitHub.