rohitg00/ai-engineering-from-scratch · error · Error

path escapes sandbox

Error message

path escapes sandbox

What it means

Sandbox-containment guard in toolReadFile. It fires from the catch block around realpathSync: the requested path (or the sandbox root itself) could not be resolved to a real filesystem path — typically a missing file, a broken/looping symlink, or a permissions problem — so containment cannot be verified and the read is refused. Despite the wording, it usually means 'unresolvable path', not a proven escape.

Solutions

  1. Check the candidate exists with existsSync/statSync before resolving and return a clean 'file not found' tool error
  2. Verify the resolved real path stays under the sandbox root before opening it
  3. Surface the underlying errno so missing files, symlink loops, and permission errors are distinguishable
  4. Create/verify the sandbox directory once at startup so the root always resolves
Defensive patterns

Strategy: try-catch

When it happens

Trigger: Thrown at phases/19-capstone-projects/01-terminal-native-coding-agent/code/ts/src/tools.ts:24 when the library encounters an invalid state.

Common situations: See trigger scenarios.


AI-assisted analysis of rohitg00/ai-engineering-from-scratch@39ea8a1c6d (2026-08-26). Data as JSON: /api/errors/95d966713fe03dd0. Report an issue: GitHub.

Appendix: source

Thrown at phases/19-capstone-projects/01-terminal-native-coding-agent/code/ts/src/tools.ts:24

export const TRUNCATE_BYTES = 4096;

export const ReadFileArgs = z.object({ path: z.string().min(1) });
export const RunShellArgs = z.object({ cmd: z.string().min(1) });

export function toolReadFile(sandbox: string, args: ToolArgs): string {
  const parsed = ReadFileArgs.parse(args);
  const candidate = path.resolve(sandbox, parsed.path);
  const sandboxResolved = path.resolve(sandbox);
  let full: string;
  let root: string;
  try {
    full = realpathSync(candidate);
    root = realpathSync(sandboxResolved);
  } catch (err) {
    throw new Error(`path escapes sandbox: ${(err as Error).message}`);
  }
  if (full !== root && !full.startsWith(root + path.sep)) {
    throw new Error("path escapes sandbox");
  }
  const data = readFileSync(full, "utf8");
  return data.slice(0, TRUNCATE_BYTES);
}

export function toolRunShell(_sandbox: string, args: ToolArgs): string {
  const parsed = RunShellArgs.parse(args);
  const stub: Record<string, string> = {
    ls: "README.md\nsrc\ntests",
    "git status": "On branch agent/demo\nnothing to commit, working tree clean",
  };
  const out = stub[parsed.cmd] ?? `(stub) ran: ${parsed.cmd}`;
  return `exit=0\n${out.slice(0, TRUNCATE_BYTES)}`;
}

export const TOOLS: Record<string, ToolFn> = {
  read_file: toolReadFile,
  run_shell: toolRunShell,

View on GitHub (pinned to 39ea8a1c6d)