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

  1. Keep benchmark baseline/output paths inside the project root (e.g. `./benchmarks/baseline.json`).
  2. If the file legitimately lives above the current package, move it inside the project or adjust `test.root` so the path is contained.
  3. Avoid `../` segments in bench.from / writeResult paths; use paths relative to the project root.
  4. 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

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.