garrytan/gstack · error · Error
File not found: ${fp}
Error message
File not found: ${fp} What it means
Thrown by `browse upload` inside the per-file validation loop when `fs.existsSync(fp)` returns false for one of the supplied file paths. The command refuses to forward a non-existent path to Playwright's `setInputFiles`, which would itself throw a less actionable error. Note the existence check runs BEFORE the safe-directory and traversal checks.
Source
Thrown at browse/src/write-commands.ts:600
case 'useragent': {
const ua = args.join(' ');
if (!ua) throw new Error('Usage: browse useragent <string>');
bm.setUserAgent(ua);
const error = await bm.recreateContext();
if (error) {
return `User agent set to "${ua}" but: ${error}`;
}
return `User agent set: ${ua}`;
}
case 'upload': {
const [selector, ...filePaths] = args;
if (!selector || filePaths.length === 0) throw new Error('Usage: browse upload <selector> <file1> [file2...]');
// Validate paths are within safe directories (same check as cookie-import)
for (const fp of filePaths) {
if (!fs.existsSync(fp)) throw new Error(`File not found: ${fp}`);
if (path.isAbsolute(fp)) {
let resolvedFp: string;
try { resolvedFp = fs.realpathSync(path.resolve(fp)); } catch (err: any) { if (err?.code !== 'ENOENT') throw err; resolvedFp = path.resolve(fp); }
if (!SAFE_DIRECTORIES.some(dir => isPathWithin(resolvedFp, dir))) {
throw new Error(`Path must be within: ${SAFE_DIRECTORIES.join(', ')}`);
}
}
if (path.normalize(fp).includes('..')) {
throw new Error('Path traversal sequences (..) are not allowed');
}
}
const resolved = await session.resolveRef(selector);
if ('locator' in resolved) {
await resolved.locator.setInputFiles(filePaths);
} else {
await target.locator(resolved.selector).setInputFiles(filePaths);
}View on GitHub (pinned to 94993f7401)
Solutions
- Verify the path exists from the same process/cwd the browse server uses: `fs.existsSync(path.resolve(fp))`.
- Expand `~` to `os.homedir()` before passing — Node does not do this for you.
- Use an absolute path under TEMP_DIR or the project cwd (those are the safe directories).
- If running in a container, ensure the file is volume-mounted into the container at the path you reference.
Example fix
// before await runBrowseCommand(['upload', 'input[type=file]', '~/Downloads/x.png']); // after import os from 'os'; import path from 'path'; const fp = path.join(os.homedir(), 'Downloads', 'x.png'); await runBrowseCommand(['upload', 'input[type=file]', fp]);
Defensive patterns
Strategy: validation
Validate before calling
import fs from 'fs';
import path from 'path';
function ensureFilesExist(filePaths: string[], cwd = process.cwd()): void {
for (const fp of filePaths) {
const resolved = path.resolve(cwd, fp);
if (!fs.existsSync(resolved)) {
throw new Error(`File not found: ${fp} (resolved: ${resolved})`);
}
}
} Type guard
function fileExists(p: string): boolean {
try { return fs.statSync(p).isFile(); } catch { return false; }
} Prevention
- Expand '~' to os.homedir() before passing paths — Node does not expand it.
- Use absolute paths resolved from the browse server's cwd, not the caller's shell cwd.
- In containers, ensure the file is volume-mounted at the referenced path.
When it happens
Trigger: Passing a relative path that does not resolve against the process cwd; a typo'd filename; a path on a different machine (remote agent context); a file that was deleted between snapshot time and upload time; a path with an unexpanded `~` (tilde is NOT expanded by Node's fs APIs).
Common situations: Agent captured an absolute path on the host but the browse server runs in a container with a different filesystem layout; user passes `~/Downloads/x.png` and Node does not expand `~`; file lives in a directory the user just `rm -rf`'d during cleanup; macOS case-insensitive FS masked a casing mismatch at authoring time but the browse server is on Linux (case-sensitive).
Related errors
- File not found: ${filePath}
- Usage: browse upload <selector> <file1> [file2...]
- Path must be within: ${SAFE_DIRECTORIES.join(', ')}
- Path traversal sequences (..) are not allowed
- Skill "${name}" not found in any tier.
AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12).
Data as JSON: /api/errors/f9ff1ff1b29d0cbb.
Report an issue: GitHub.