JuliusBrussee/caveman · error · Error

caveman agent: dev ${kind} escapes project root

Error message

caveman agent: dev ${kind} escapes project root

What it means

Thrown by copyProjectFile in the dev loader (packages/agent/src/dev-loader.ts:146): while staging the immutable dev snapshot, a project-relative source path (a FileSource instruction/context file or a source-graph module) resolves outside the project root. Dev staging only copies files under the project root, so any '..'-escaping or absolute path is rejected.

Source

Thrown at packages/agent/src/dev-loader.ts:146

async function copyOptional(source: string, destination: string): Promise<void> {
  try {
    await readFile(source);
    await mkdir(dirname(destination), { recursive: true });
    await copyFile(source, destination);
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
  }
}

async function copyProjectFile(
  root: string,
  staging: string,
  source: string,
  kind: "source graph" | "file source",
): Promise<void> {
  const name = relative(root, source);
  if (escapesRoot(name)) {
    throw new Error(`caveman agent: dev ${kind} escapes project root`);
  }
  const canonicalSource = await realpath(source);
  if (escapesRoot(relative(root, canonicalSource))) {
    throw new Error(`caveman agent: dev ${kind} symlink escapes project root`);
  }
  const destination = resolve(staging, name);
  await mkdir(dirname(destination), { recursive: true });
  await copyFile(canonicalSource, destination);
}

function remapDevError(error: unknown, staging: string, root: string): unknown {
  if (!(error instanceof Error)) return error;
  const mapped = new Error(error.message, { cause: error.cause });
  mapped.name = error.name;
  mapped.stack = (error.stack ?? `${error.name}: ${error.message}`).replaceAll(staging, root);
  return mapped;
}

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Move the referenced file inside the project root and reference it with a root-relative path.
  2. If the file legitimately lives elsewhere in a monorepo, widen the configured project/dev root so the file is inside it.
  3. Inline the content as a string instruction instead of a FileSource if the file cannot be moved.

Example fix

// before
agent({ instructions: file("../shared/prompts/system.md"), /* ... */ });

// after
agent({ instructions: file("prompts/system.md"), /* ... */ }); // move the file under the project root
Defensive patterns

Strategy: validation

Validate before calling

import { relative, isAbsolute } from "node:path";
function escapesRoot(root: string, source: string): boolean {
  const rel = relative(root, source);
  return rel === ".." || rel.startsWith("..") || isAbsolute(rel);
}
// check every FileSource path before dev/build:
for (const p of agentFileSourcePaths(definition)) {
  if (escapesRoot(projectRoot, p)) throw new Error(`move ${p} inside ${projectRoot}`);
}

Prevention

When it happens

Trigger: A FileSource path (agent instructions or context source) pointing outside the root via '../', or an absolute path; or a module pulled into the project source graph that lives outside the root. The message tells you whether it came from the 'source graph' or a 'file source'.

Common situations: Instructions loaded from a shared monorepo package or a home-directory prompt file; contexts referencing ../../docs; workspaces where the agent root was configured as a subdirectory while imports reach a sibling package; passing an absolute path like /etc/... or /home/user/prompt.md.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/c505457db6544590. Report an issue: GitHub.