garrytan/gstack · error · Error

Cannot resolve real path: ${screenshotPath} (${err.code})

Error message

Cannot resolve real path: ${screenshotPath} (${err.code})

What it means

Thrown by the annotated-screenshot path validator in snapshot.ts. realpathSync() failed on the screenshot output path with an error code other than ENOENT (which is handled separately). The message echoes the failing path and the fs error code (e.g. EACCES, ELOOP, ENOTDIR). It guards the screenshot writer against unreadable, looping, or non-directory segments so the path can never be dereferenced blindly.

Source

Thrown at browse/src/snapshot.ts:380

      const nodeFs = require('fs') as typeof import('fs');
      const absolute = nodePath.resolve(screenshotPath);
      const safeDirs = [TEMP_DIR, process.cwd()].map((d: string) => {
        try { return nodeFs.realpathSync(d); } catch (err: any) { if (err?.code !== 'ENOENT') throw err; return d; }
      });
      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;

View on GitHub (pinned to 94993f7401)

Solutions

  1. Re-run the same command with --output-path pointing at a directory you own (e.g. inside the current working directory or the system temp dir).
  2. Inspect the error code in parentheses: EACCES → fix permissions/ownership of every parent dir; ELOOP → remove the offending symlink loop; ENOTDIR → make sure every prefix segment except the final filename is a directory.
  3. Drop --output-path entirely so snapshot.ts falls back to `${TEMP_DIR}/browse-annotated.png`, which is always writable.
  4. If running under systemd/launchd/container, confirm the service user has read+resolve rights on the full realpath chain, not just the leaf.

Example fix

// before
snapshot(page, { annotate: true, outputPath: '/root/protected/shot.png' });
// after
snapshot(page, { annotate: true }); // writes to TEMP_DIR/browse-annotated.png
Defensive patterns

Strategy: validation

Validate before calling

const fs = require('fs');
const path = require('path');
function assertScreenshotPathWritable(p: string, safeDirs: string[]): void {
  const abs = path.resolve(p);
  // Walk the path and verify each prefix is a directory we can traverse
  const dirs = abs.split(path.sep).reduce<string[]>((acc, seg, i) => {
    if (i === 0) return [seg || '/'];
    return [...acc, path.join(acc[acc.length - 1], seg)];
  }, []).slice(0, -1); // exclude the file itself
  for (const d of dirs) {
    try { const st = fs.statSync(d); if (!st.isDirectory()) throw new Error(`ENOTDIR: ${d}`); }
    catch (e: any) { throw new Error(`Cannot resolve real path: ${p} (parent ${d} unreachable: ${e.code ?? e.message})`); }
  }
}

Try / catch

try {
  await snapshot(session, { annotate: true, outputPath });
} catch (e: any) {
  if (/^Cannot resolve real path:/.test(e.message)) {
    // fall back to the default temp location and retry
    outputPath = `${require('os').tmpdir()}/browse-annotated.png`;
    await snapshot(session, { annotate: true, outputPath });
  } else throw e;
}

Prevention

When it happens

Trigger: Calling snapshot with opts.annotate=true and an opts.outputPath whose parent chain hits a permission wall, a symlink loop, or a non-directory segment. Only thrown when fs.realpathSync rejects with a code that is NOT 'ENOENT' — ENOENT falls through to the parent-dir fallback at line 372-378.

Common situations: Running the browse binary under a user that cannot traverse a directory in the --output-path; pointing --output-path at a symlink cycle; passing a path whose prefix is a file rather than a directory (ENOTDIR); SELinux/AppArmor denying realpath on the temp dir.

Related errors


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