garrytan/gstack · warning · Error

Usage: browse useragent <string>

Error message

Usage: browse useragent <string>

What it means

Thrown by `browse useragent` when the joined args string is empty. The command joins all positional args with spaces to form the user agent string; if no args were supplied, `ua` is `''` which is falsy, and the command refuses to set an empty user agent (which Playwright would treat as a deliberate override to empty, breaking fingerprinting and likely site behavior).

Source

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

      }]);
      return `Cookie set: ${name}=****`;
    }

    case 'header': {
      const headerStr = args[0];
      if (!headerStr || !headerStr.includes(':')) throw new Error('Usage: browse header <name>:<value>');
      const sep = headerStr.indexOf(':');
      const name = headerStr.slice(0, sep).trim();
      const value = headerStr.slice(sep + 1).trim();
      await bm.setExtraHeader(name, value);
      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); }

View on GitHub (pinned to 94993f7401)

Solutions

  1. Provide a full UA string, quoting it so the shell does not split it: `browse useragent "Mozilla/5.0 ..."`.
  2. If you want the default Playwright UA back, recreate the context without a custom UA via the appropriate BrowserManager path rather than passing empty.
  3. Capture the UA from `navigator.userAgent` BEFORE any context recreation so the value is populated.
  4. If scripting, assert the UA variable is non-empty before dispatching the command.

Example fix

// before
const ua = readUaMaybe(); // may be ''
await runBrowseCommand(['useragent', ua]);

// after
const ua = await page.evaluate(() => navigator.userAgent);
if (!ua) throw new Error('UA read failed');
await runBrowseCommand(['useragent', ua]);
Defensive patterns

Strategy: validation

Validate before calling

function validateUserAgentArg(args: string[]): string {
  const ua = args.join(' ').trim();
  if (!ua) throw new Error('useragent requires a non-empty UA string');
  return ua;
}

Type guard

function isNonEmptyString(s: unknown): s is string {
  return typeof s === 'string' && s.trim().length > 0;
}

Prevention

When it happens

Trigger: Calling `browse useragent` with no arguments; passing only whitespace tokens that join to an empty string; forwarding a UA variable that resolved to empty.

Common situations: An agent reads `navigator.userAgent`, intends to re-apply it after context recreation, but the read happened before navigation completed and returned empty; a user wants to RESET to the default UA and assumes omitting the value does that (it does not — there is no reset path through this command); copy-paste from a config where the UA string lived on the next line and was dropped.

Related errors


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