{"record":{"id":"6da36ce7e4dfe844","repo":"garrytan/gstack","slug":"cannot-resolve-real-path-screenshotpath-err","errorCode":null,"errorMessage":"Cannot resolve real path: ${screenshotPath} (${err.code})","messagePattern":"Cannot resolve real path: (.+?) \\((.+?)\\)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"browse/src/snapshot.ts","lineNumber":380,"sourceCode":"      const nodeFs = require('fs') as typeof import('fs');\n      const absolute = nodePath.resolve(screenshotPath);\n      const safeDirs = [TEMP_DIR, process.cwd()].map((d: string) => {\n        try { return nodeFs.realpathSync(d); } catch (err: any) { if (err?.code !== 'ENOENT') throw err; return d; }\n      });\n      let realPath: string;\n      try {\n        realPath = nodeFs.realpathSync(absolute);\n      } catch (err: any) {\n        if (err.code === 'ENOENT') {\n          try {\n            const dir = nodeFs.realpathSync(nodePath.dirname(absolute));\n            realPath = nodePath.join(dir, nodePath.basename(absolute));\n          } catch (err2: any) {\n            if (err2?.code !== 'ENOENT') throw err2;\n            realPath = absolute;\n          }\n        } else {\n          throw new Error(`Cannot resolve real path: ${screenshotPath} (${err.code})`);\n        }\n      }\n      if (!safeDirs.some((dir: string) => isPathWithin(realPath, dir))) {\n        throw new Error(`Path must be within: ${safeDirs.join(', ')}`);\n      }\n    }\n    try {\n      // Inject overlay divs at each ref's bounding box\n      const boxes: Array<{ ref: string; box: { x: number; y: number; width: number; height: number } }> = [];\n      for (const [ref, entry] of refMap) {\n        try {\n          const box = await entry.locator.boundingBox({ timeout: 1000 });\n          if (box) {\n            boxes.push({ ref: `@${ref}`, box });\n          }\n        } catch (err: any) {\n          // Element may be offscreen, hidden, or page navigated — skip\n          if (!err?.message?.includes('Timeout') && !err?.message?.includes('timeout') && !err?.message?.includes('closed') && !err?.message?.includes('Target') && !err?.message?.includes('Execution context')) throw err;","sourceCodeStart":362,"sourceCodeEnd":398,"githubUrl":"https://github.com/garrytan/gstack/blob/94993f74012782fd94416dd44b8314f6363a13a4/browse/src/snapshot.ts#L362-L398","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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).","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.","Drop --output-path entirely so snapshot.ts falls back to `${TEMP_DIR}/browse-annotated.png`, which is always writable.","If running under systemd/launchd/container, confirm the service user has read+resolve rights on the full realpath chain, not just the leaf."],"exampleFix":"// before\nsnapshot(page, { annotate: true, outputPath: '/root/protected/shot.png' });\n// after\nsnapshot(page, { annotate: true }); // writes to TEMP_DIR/browse-annotated.png","handlingStrategy":"validation","validationCode":"const fs = require('fs');\nconst path = require('path');\nfunction assertScreenshotPathWritable(p: string, safeDirs: string[]): void {\n  const abs = path.resolve(p);\n  // Walk the path and verify each prefix is a directory we can traverse\n  const dirs = abs.split(path.sep).reduce<string[]>((acc, seg, i) => {\n    if (i === 0) return [seg || '/'];\n    return [...acc, path.join(acc[acc.length - 1], seg)];\n  }, []).slice(0, -1); // exclude the file itself\n  for (const d of dirs) {\n    try { const st = fs.statSync(d); if (!st.isDirectory()) throw new Error(`ENOTDIR: ${d}`); }\n    catch (e: any) { throw new Error(`Cannot resolve real path: ${p} (parent ${d} unreachable: ${e.code ?? e.message})`); }\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  await snapshot(session, { annotate: true, outputPath });\n} catch (e: any) {\n  if (/^Cannot resolve real path:/.test(e.message)) {\n    // fall back to the default temp location and retry\n    outputPath = `${require('os').tmpdir()}/browse-annotated.png`;\n    await snapshot(session, { annotate: true, outputPath });\n  } else throw e;\n}","preventionTips":["Always pass an --output-path inside process.cwd() or the system temp dir.","Run `readlink -f` on the output path before invoking snapshot to catch symlink loops and permission walls early.","In CI, run the browse process as the same user that owns the output directory.","Default outputPath to undefined and let snapshot.ts pick the temp location."],"tags":["filesystem","permissions","path-resolution","screenshot","realpath"],"backgroundTag":null,"analyzedSha":"94993f74012782fd94416dd44b8314f6363a13a4","analyzedAt":"2026-08-12T04:06:23.140Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}