garrytan/gstack · error · Error

Path must be within: ${safeDirs.join(', ')}

Error message

Path must be within: ${safeDirs.join(', ')}

What it means

Thrown by the annotated-screenshot sandbox check in snapshot.ts. After resolving the realpath of the requested output path, the code asserts it lives inside one of the safe directories (TEMP_DIR or process.cwd()). If realpath escaped both — e.g. via a symlink or an absolute --output-path elsewhere on disk — the write is refused before page.screenshot() is ever called.

Source

Thrown at browse/src/snapshot.ts:384

      });
      let realPath: string;
      try {
        realPath = nodeFs.realpathSync(absolute);
      } catch (err: any) {
        if (err.code === 'ENOENT') {
          try {
            const dir = nodeFs.realpathSync(nodePath.dirname(absolute));
            realPath = nodePath.join(dir, nodePath.basename(absolute));
          } catch (err2: any) {
            if (err2?.code !== 'ENOENT') throw err2;
            realPath = absolute;
          }
        } else {
          throw new Error(`Cannot resolve real path: ${screenshotPath} (${err.code})`);
        }
      }
      if (!safeDirs.some((dir: string) => isPathWithin(realPath, dir))) {
        throw new Error(`Path must be within: ${safeDirs.join(', ')}`);
      }
    }
    try {
      // Inject overlay divs at each ref's bounding box
      const boxes: Array<{ ref: string; box: { x: number; y: number; width: number; height: number } }> = [];
      for (const [ref, entry] of refMap) {
        try {
          const box = await entry.locator.boundingBox({ timeout: 1000 });
          if (box) {
            boxes.push({ ref: `@${ref}`, box });
          }
        } catch (err: any) {
          // Element may be offscreen, hidden, or page navigated — skip
          if (!err?.message?.includes('Timeout') && !err?.message?.includes('timeout') && !err?.message?.includes('closed') && !err?.message?.includes('Target') && !err?.message?.includes('Execution context')) throw err;
        }
      }

      await page.evaluate((boxes) => {

View on GitHub (pinned to 94993f7401)

Solutions

  1. Point --output-path at a location physically inside the current working directory (verify with `readlink -f`).
  2. Remove or rewrite symlinks in the output path that escape the safe dirs.
  3. Launch the browse process from the directory you want outputs to land in so process.cwd() covers it.
  4. Set TMPDIR to a writable temp area you control if you need the TEMP_DIR branch.

Example fix

// before
snapshot(page, { annotate: true, outputPath: '/var/www/shot.png' }); // outside cwd & TEMP_DIR
// after
snapshot(page, { annotate: true, outputPath: './shots/shot.png' }); // inside cwd
Defensive patterns

Strategy: validation

Validate before calling

const fs = require('fs');
const path = require('path');
function assertPathWithin(p: string, safeDirs: string[]): void {
  const real = fs.realpathSync(p); // throws if not resolvable
  const ok = safeDirs.some(d => real === d || real.startsWith(d + path.sep));
  if (!ok) throw new Error(`Path must be within: ${safeDirs.join(', ')}`);
}

Try / catch

try {
  await snapshot(session, { annotate: true, outputPath });
} catch (e: any) {
  if (/^Path must be within:/.test(e.message)) {
    outputPath = path.join(process.cwd(), 'browse-annotated.png');
    await snapshot(session, { annotate: true, outputPath });
  } else throw e;
}

Prevention

When it happens

Trigger: Passing opts.outputPath (or letting it default to an absolute path) whose realpath resolves outside [TEMP_DIR, process.cwd()]; a symlink inside cwd that points to /etc or another tree; running the CLI from a working directory that itself symlinks elsewhere after realpathSync.

Common situations: Hard-coding an --output-path like '/tmp/other-tool/shot.png' where /tmp/other-tool is a symlink to /var/www; chaining snapshots across processes that share a cwd but write to different roots; container mounts where cwd resolves through a symlink to a host path.

Related errors


AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12). Data as JSON: /api/errors/16f4206d0c6cf9dc. Report an issue: GitHub.