vitest-dev/vitest · error · Error
Benchmark artifact path "${relativePath}" resolves outside t
Error message
Benchmark artifact path "${relativePath}" resolves outside the project root (${root}). Paths passed to `writeResult` and `bench.from()` must point inside the project. What it means
A path-traversal guard in BenchmarkManager.resolve(). Both bench.from() (reads a baseline) and writeResult (writes a benchmark artifact) resolve the user-supplied path against the project root and reject any result that escapes it. This prevents a benchmark config or baseline path from reading or clobbering files outside the workspace.
Source
Thrown at packages/vitest/src/node/benchmark.ts:21
import { existsSync } from 'node:fs'
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { dirname, isAbsolute, resolve } from 'pathe'
export class BenchmarkManager {
constructor(private project: TestProject) {}
// Resolve a user-supplied path against the project root. Reject paths that
// escape the project root: `bench.from()` accepts arbitrary input, and we
// never want a benchmark file to be able to read or clobber files outside
// the workspace.
public resolve(relativePath: string): string {
const root = this.project.config.root
const absolute = isAbsolute(relativePath)
? resolve(relativePath)
: resolve(root, relativePath)
const rootWithSep = root.endsWith('/') ? root : `${root}/`
if (absolute !== root && !absolute.startsWith(rootWithSep)) {
throw new Error(
`Benchmark artifact path "${relativePath}" resolves outside the project root (${root}). `
+ `Paths passed to \`writeResult\` and \`bench.from()\` must point inside the project.`,
)
}
return absolute
}
async readResult(relativePath: string): Promise<BaselineData | null> {
const path = this.resolve(relativePath)
if (!existsSync(path)) {
return null
}
return JSON.parse(await readFile(path, 'utf-8')) as BaselineData
}
async writeResult(relativePath: string, data: BaselineData): Promise<void> {
const absolute = this.resolve(relativePath)
await mkdir(dirname(absolute), { recursive: true })View on GitHub (pinned to d568f8ce37)
Solutions
- Keep benchmark baseline/output paths inside the project root (e.g. `./benchmarks/baseline.json`).
- If the file legitimately lives above the current package, move it inside the project or adjust `test.root` so the path is contained.
- Avoid `../` segments in bench.from / writeResult paths; use paths relative to the project root.
- If you need cross-package baselines in a monorepo, configure each project's root to be the workspace root or copy the baseline into each package.
Example fix
// before
bench: { writeResult: () => writeResult('../shared/baseline.json') }
// after
bench: { writeResult: () => writeResult('./baseline.json') } Defensive patterns
Strategy: validation
Validate before calling
import { isAbsolute, resolve, relative } from 'node:path'
function assertInsideRoot(root: string, p: string) {
const abs = isAbsolute(p) ? p : resolve(root, p)
const rel = relative(root, abs)
if (rel.startsWith('..')) throw new Error(`path escapes root: ${p}`)
} Prevention
- Keep benchmark baseline/output paths relative to the project root with no `..` segments.
- Validate paths in your bench config builder before passing to writeResult/bench.from.
- In monorepos, set test.root so legitimate cross-package paths stay contained.
When it happens
Trigger: Configuring `bench.from` or the benchmark output path with a value containing `../` that climbs above root, or an absolute path pointing outside the project root. Triggered when BenchmarkManager.resolve() computes an absolute path that is neither equal to root nor prefixed by `root/`.
Common situations: Setting bench baseline/output to `../../benchmarks/baseline.json`; symlinking the output dir outside the project; misconfigured monorepo where root is a sub-package but the baseline lives in the workspace root above it; CI that passes an absolute temp path.
AI-assisted analysis of vitest-dev/vitest@d568f8ce37 (2026-08-03).
Data as JSON: /data/errors/ee6f9da4f6595794.json.
Report an issue: GitHub.