nexu-io/open-design · error · Error

--image path "${rel}" resolves outside the project directory

Error message

--image path "${rel}" resolves outside the project directory.

What it means

Thrown by resolveProjectImage, a security guard in apps/daemon/src/media/index.ts. The resolved absolute path of an `--image` argument must equal or sit beneath the project directory; any path that escapes (via `..`, absolute paths, or symlink resolution) is rejected before stat/readFile. This blocks an agent or hallucinated arg from uploading arbitrary host files (e.g. /etc/passwd) to a paid model.

Source

Thrown at apps/daemon/src/media/index.ts:226

/**
 * Resolve a project-relative `--image` path into a base64 data URL the
 * upstream model APIs (Volcengine i2v, OpenAI image-edit, etc.) accept
 * directly. Returns null when no path was supplied.
 *
 * Security: refuses anything that escapes the project directory.
 * Without this guard, an agent (or a hallucinated arg) could ask the
 * daemon to upload `/etc/passwd` to a paid model.
 */
async function resolveProjectImage(rel: unknown, projectDir: string): Promise<ImageRef | null> {
  if (typeof rel !== 'string' || !rel.trim()) return null;
  const projectRootResolved = path.resolve(projectDir);
  const abs = path.resolve(projectRootResolved, rel.trim());
  if (
    abs !== projectRootResolved &&
    !abs.startsWith(projectRootResolved + path.sep)
  ) {
    throw new Error(
      `--image path "${rel}" resolves outside the project directory.`,
    );
  }
  let info;
  try {
    info = await stat(abs);
  } catch {
    throw new Error(`--image not found: ${rel}`);
  }
  if (!info.isFile()) {
    throw new Error(`--image is not a regular file: ${rel}`);
  }
  // Cap at 16 MB. Beyond this, base64 inflation alone (≈4/3) starts
  // hitting body-size limits at the upstream APIs and our own express
  // 4mb body cap on inbound requests; bigger payloads should travel
  // via the dedicated upload endpoint, not the dispatcher.
  const MAX_IMAGE_BYTES = 16 * 1024 * 1024;
  if (info.size > MAX_IMAGE_BYTES) {

View on GitHub (pinned to 5be4028344)

Solutions

  1. Reference images by a path relative to the project root and keep them inside the project directory.
  2. Copy the external image into the project's assets folder first, then reference it.
  3. Remove or relocate symlinks that escape the project root.
  4. Audit agent-supplied --image args against the project root before dispatch.

Example fix

// before
--image /home/user/pic.png
// after: place under project first
--image assets/pic.png
Defensive patterns

Strategy: validation

Validate before calling

const abs = path.resolve(projectDir, rel.trim());
if (abs !== projectDir && !abs.startsWith(projectDir + path.sep)) {
  throw new Error('image path escapes project dir');
}

Type guard

function isWithinProject(rel: string, projectDir: string): boolean {
  const abs = path.resolve(projectDir, rel.trim());
  return abs === projectDir || abs.startsWith(projectDir + path.sep);
}

Prevention

When it happens

Trigger: Passing `--image ../../../etc/passwd` or `--image /etc/shadow`; a symlink inside the project that points outside; an absolute path that is not under the project root; path components that normalize above the project root.

Common situations: Agent hallucinates a system path as an image source; symlinked assets pointing outside the workspace; user pastes an absolute path from elsewhere on the machine.

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/6a5c1d7c5aebebb1. Report an issue: GitHub.