garrytan/gstack · error · Error

File not found

Error message

File not found: ${filePath}

What it means

Thrown by `browse cookie-import` when the path passes the safe-directory check but `fs.existsSync(filePath)` returns false. The existence check runs AFTER the security check by design — security is validated on the resolved path even for non-existent files, so a probing caller cannot learn whether arbitrary paths exist. This ordering prevents information leakage about the filesystem outside the safe roots.

Solutions

  1. Verify the file exists from the browse server's process: `fs.existsSync(path.resolve(filePath))`.
  2. If the file was in TEMP_DIR, it may have been reaped — re-export and re-import in the same session.
  3. Expand `~` to `os.homedir()` (and ensure homedir is inside a safe directory, or stage to TEMP_DIR).
  4. Pass an absolute path to avoid cwd ambiguity.

Example fix

// before
await runBrowseCommand(['cookie-import', '~/cookies.json']);

// after
import fs from 'fs';
import os from 'os';
import path from 'path';
const staged = path.join(os.tmpdir(), 'cookies.json');
fs.copyFileSync(path.join(os.homedir(), 'cookies.json'), staged);
await runBrowseCommand(['cookie-import', staged]);
Defensive patterns

Strategy: validation

Validate before calling

import fs from 'fs';
import path from 'path';
function ensureImportFileExists(filePath: string): void {
  if (!fs.existsSync(filePath)) {
    throw new Error(`File not found: ${filePath}`);
  }
}

Type guard

function fileExists(p: string): boolean {
  try { return fs.statSync(p).isFile(); } catch { return false; }
}

Prevention

When it happens

Trigger: The JSON file was deleted between the safe-directory check and the existence check; a typo in the filename; a relative path that does not resolve against cwd; an unexpanded `~`; the file lives in TEMP_DIR but under a different name.

Common situations: Agent exported cookies to a temp file, the temp dir was cleaned (cron tmpwatch, container restart), and the import runs later; user passes a relative path expecting it to resolve against their shell's cwd but the browse server has a different cwd; macOS case-insensitivity masked a casing error.

Related errors


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

Appendix: source

Thrown at browse/src/write-commands.ts:657

      return 'Dialogs will be dismissed';
    }

    case 'cookie-import': {
      const filePath = args[0];
      if (!filePath) throw new Error('Usage: browse cookie-import <json-file>');
      // Path validation — resolve to absolute and check against safe dirs.
      // Fixes #707: relative paths previously bypassed the safe directory check.
      // Mirrors validateOutputPath() — resolves symlinks (e.g., macOS /tmp → /private/tmp).
      const resolved = path.resolve(filePath);
      let resolvedReal = resolved;
      try { resolvedReal = fs.realpathSync(resolved); } catch {
        // File may not exist yet — resolve parent dir instead
        try { resolvedReal = path.join(fs.realpathSync(path.dirname(resolved)), path.basename(resolved)); } catch {}
      }
      if (!SAFE_DIRECTORIES.some(dir => isPathWithin(resolvedReal, dir))) {
        throw new Error(`Path must be within: ${SAFE_DIRECTORIES.join(', ')}`);
      }
      if (!fs.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
      const raw = fs.readFileSync(filePath, 'utf-8');
      let cookies: any[];
      try { cookies = JSON.parse(raw); } catch (err: any) { throw new Error(`Invalid JSON in ${filePath}: ${err?.message || err}`); }
      if (!Array.isArray(cookies)) throw new Error('Cookie file must contain a JSON array');

      // Auto-fill domain from current page URL when missing (consistent with cookie command)
      const pageUrl = new URL(page.url());
      const defaultDomain = pageUrl.hostname;

      for (const c of cookies) {
        if (!c.name || c.value === undefined) throw new Error('Each cookie must have "name" and "value" fields');
        if (!c.domain) {
          c.domain = defaultDomain;
        } else {
          const cookieDomain = c.domain.startsWith('.') ? c.domain.slice(1) : c.domain;
          if (cookieDomain !== defaultDomain && !defaultDomain.endsWith('.' + cookieDomain)) {
            throw new Error(`Cookie domain "${c.domain}" does not match current page domain "${defaultDomain}". Use the target site first.`);
          }

View on GitHub (pinned to 94993f7401)