garrytan/gstack · warning · Error

Usage: browse upload <selector> <file1> [file2...]

Error message

Usage: browse upload <selector> <file1> [file2...]

What it means

Thrown by `browse upload` when no selector is supplied OR when no file paths remain after the selector. The command destructures `[selector, ...filePaths]` from args; both a non-empty selector and at least one file path are mandatory because Playwright's `setInputFiles` needs a target `<input type=file>` and the files to set.

Source

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

      const sensitiveHeaders = ['authorization', 'cookie', 'set-cookie', 'x-api-key', 'x-auth-token'];
      const redactedValue = sensitiveHeaders.includes(name.toLowerCase()) ? '****' : value;
      return `Header set: ${name}: ${redactedValue}`;
    }

    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) {

View on GitHub (pinned to 94993f7401)

Solutions

  1. Provide both a selector and at least one file: `browse upload input[type=file] /tmp/errlookup-0o5UJv/upload.png`.
  2. If the selector contains spaces, quote it so it stays a single arg: `browse upload "input[data-uploader]" file.png`.
  3. For multi-file inputs, pass multiple paths: `browse upload #files a.png b.png`.
  4. Confirm the selector targets an `<input type=file>` — `setInputFiles` only works on file inputs.

Example fix

// before
await runBrowseCommand(['upload', '/tmp/errlookup-0o5UJv/upload.png']);

// after
await runBrowseCommand(['upload', 'input[type=file]', '/tmp/errlookup-0o5UJv/upload.png']);
Defensive patterns

Strategy: validation

Validate before calling

function validateUploadArgs(args: string[]): { selector: string; filePaths: string[] } {
  const [selector, ...filePaths] = args;
  if (!selector || filePaths.length === 0) {
    throw new Error('upload requires <selector> <file1> [file2...]');
  }
  return { selector, filePaths };
}

Type guard

function isUploadArgs(args: unknown): args is [string, string, ...string[]] {
  return Array.isArray(args) && typeof args[0] === 'string' && args.length >= 2;
}

Prevention

When it happens

Trigger: Calling `browse upload` with no args; `browse upload file.txt` (selector omitted, so the file is misread as the selector and filePaths is empty); `browse upload input#file` (selector present but no files).

Common situations: An agent resolves the file input selector from a snapshot but the file path argument was dropped during JSON marshalling; a user expects `browse upload file.txt` to auto-locate the input element; the selector was resolved but contained a space and got split, consuming the file path as part of the selector.

Related errors


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