{"record":{"id":"5eea8ee6a8039361","repo":"JuliusBrussee/caveman","slug":"cave-live-eval-sandbox-profile-escapes-root","errorCode":null,"errorMessage":"cave_live_eval_sandbox_profile_escapes_root","messagePattern":"cave_live_eval_sandbox_profile_escapes_root","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/agent/src/cli.ts","lineNumber":1240,"sourceCode":"    throw new Error(\"cave_fixture_terminal_evidence_missing\", { cause: error });\n  }\n}\n\nasync function loadEvalSandboxProfile(\n  root: string,\n  fixture: EvalDefinition,\n): Promise<{\n  network: boolean;\n  childProcess: boolean;\n  credentialEnv: readonly string[];\n}> {\n  const path = fixture.tools.sandbox;\n  if (!path) throw new Error(\"cave_live_eval_sandbox_profile_missing\");\n  const fullPath = resolve(root, path);\n  const relativePath = relative(root, fullPath);\n  if (relativePath === \"..\" || relativePath.startsWith(\"../\") ||\n      relativePath.startsWith(\"..\\\\\") || isAbsolute(relativePath)) {\n    throw new Error(\"cave_live_eval_sandbox_profile_escapes_root\");\n  }\n  const parsed = JSON.parse(await readFile(fullPath, \"utf8\")) as Record<string, unknown>;\n  const keys = Object.keys(parsed).sort();\n  const expected = [\"child_process\", \"credential_env\", \"network\", \"schema_version\"];\n  if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index]) ||\n      parsed.schema_version !== 1 || typeof parsed.network !== \"boolean\" ||\n      typeof parsed.child_process !== \"boolean\" || !Array.isArray(parsed.credential_env) ||\n      parsed.credential_env.some((name) => typeof name !== \"string\" ||\n        !/^[A-Z][A-Z0-9_]{0,127}$/.test(name))) {\n    throw new Error(\"cave_live_eval_sandbox_profile_invalid\");\n  }\n  validateSandboxCredentialEnv(parsed.credential_env as string[]);\n  return {\n    network: parsed.network,\n    childProcess: parsed.child_process,\n    credentialEnv: parsed.credential_env as string[],\n  };\n}","sourceCodeStart":1222,"sourceCodeEnd":1258,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/cli.ts#L1222-L1258","documentation":"Thrown by `loadEvalSandboxProfile` when the fixture's sandbox profile path, resolved against the project root, escapes the root: `relative(root, fullPath)` starts with `..`, equals `..`, or is absolute. The sandbox profile is trusted security configuration, so it must live inside the project; a path pointing outside (absolute path, or `../` traversal, including Windows backslash variants) is rejected before the JSON is ever read.","triggerScenarios":"A fixture with `tools.sandbox: \"/etc/sandbox.json\"` or `\"../../shared/sandbox.json\"`. Also reachable via symlinks that make the resolved absolute path fall outside root — realpath resolution means a link pointing out of the workspace is out.","commonSituations":"Monorepo teams pointing evals at a shared profile outside the package directory; absolute paths authored on one machine breaking on another; symlinked config directories (dotfiles) whose realpath lives outside the repo.","solutions":["Move the sandbox profile inside the project root and reference it with a root-relative path.","For shared monorepo profiles, place them under the package root or copy/sync them in during build.","Avoid absolute paths in eval definitions — they also break portability across machines."],"exampleFix":"// before\ntools: { sandbox: \"../../shared/sandbox.json\" }\n\n// after: copy the profile into the project\ntools: { sandbox: \"evals/sandbox.json\" }","handlingStrategy":"validation","validationCode":"import { relative, isAbsolute, resolve } from \"node:path\";\n\nfunction sandboxPathInRoot(root: string, p: string): boolean {\n  const rel = relative(root, resolve(root, p));\n  return rel !== \"..\" && !rel.startsWith(\"..\") && !rel.startsWith(\"..\\\\\") && !isAbsolute(rel);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await build(args);\n} catch (error) {\n  if (error instanceof Error && error.message === \"cave_live_eval_sandbox_profile_escapes_root\") {\n    // move the profile under the project root and use a relative path\n  } else throw error;\n}","preventionTips":["Keep sandbox profiles inside the package root; never absolute paths.","Remember containment is realpath-based: symlinks out of the project count as escapes.","In monorepos, sync shared profiles into each package instead of reaching up the tree."],"tags":["sandbox","security","path-traversal","eval"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}