garrytan/gstack · error · Error

State name must be alphanumeric (a-z, 0-9, _, -)

Error message

State name must be alphanumeric (a-z, 0-9, _, -)

What it means

The `state` command sanitizes `name` against `^[a-zA-Z0-9_-]+$` (lines 930-932) before joining it into a filesystem path (`path.join(stateDir, name + '.json')`). This prevents path traversal (`../`) and weird filenames. Any other character — slash, dot, space, unicode — triggers the error.

Source

Thrown at browse/src/meta-commands.ts:932

      // Handle --clear flag
      if (args.includes('--clear')) {
        for (const file of files) {
          try { fs.unlinkSync(path.join(inboxDir, file)); } catch (err: any) { if (err?.code !== 'ENOENT') throw err; }
        }
        lines.push(`Cleared ${files.length} message${files.length === 1 ? '' : 's'}.`);
      }

      return lines.join('\n');
    }

    // ─── State ────────────────────────────────────────
    case 'state': {
      const [action, name] = args;
      if (!action || !name) throw new Error('Usage: state save|load <name>');

      // Sanitize name: alphanumeric + hyphens + underscores only
      if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
        throw new Error('State name must be alphanumeric (a-z, 0-9, _, -)');
      }

      const config = resolveConfig();
      const stateDir = path.join(config.stateDir, 'browse-states');
      mkdirSecure(stateDir);
      const statePath = path.join(stateDir, `${name}.json`);

      if (action === 'save') {
        const state = await bm.saveState();
        // V1: cookies + URLs only (not localStorage — breaks on load-before-navigate)
        const saveData = {
          version: 1,
          savedAt: new Date().toISOString(),
          cookies: state.cookies,
          pages: state.pages.map(p => ({ url: p.url, isActive: p.isActive })),
        };
        writeSecureFile(statePath, JSON.stringify(saveData, null, 2));
        return `State saved: ${statePath} (${state.cookies.length} cookies, ${state.pages.length} pages)\n⚠️  Cookies stored in plaintext. Delete when no longer needed.`;

View on GitHub (pinned to 94993f7401)

Solutions

  1. Use only letters, digits, underscore, and hyphen: `login-session`, `admin_v1`.
  2. Replace separators: use `-` or `_` instead of `/` or `.`.
  3. If you need hierarchy, encode it in the name (e.g. `tenantA-user`) rather than path segments.

Example fix

// before
browse state save tenantA/user.v2
// after
browse state save tenantA-user-v2
Defensive patterns

Strategy: validation

Validate before calling

if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
  throw new Error(`Invalid state name '${name}': use only a-z, 0-9, _, -`);
}

Type guard

const isStateName = (s: string): boolean => /^[a-zA-Z0-9_-]+$/.test(s);

Prevention

When it happens

Trigger: `browse state save my/session`, `browse state save ../etc`, `browse state save session.2`, or any name containing spaces, dots, slashes, or non-ASCII.

Common situations: Trying to namespace states with slashes/dots, or passing a filename instead of a logical name.

Related errors


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