Skip to content
Back to skills

Browse

ASecurity

Fast headless browser for QA testing and site dogfooding. (gstack)

  • 2 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 19, 2026
ai-agentsjavascriptgojavabashtestingdebugginggitsecurity

Works with

  • cursor
  • cli

Security analysis

A92/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned September 19, 2026

npx -y skills add alvaroofernaandez/dotfiles --skill browse --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Browse?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Browse
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/alvaroofernaandez-browse/badge)](https://www.skillsdirectory.com/skills/alvaroofernaandez-browse)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: browse
preamble-tier: 1
version: 1.1.0
description: Fast headless browser for QA testing and site dogfooding. (gstack)
triggers:
  - browse a page
  - headless browser
  - take page screenshot
allowed-tools:
  - Bash
  - Read
  - AskUserQuestion

---

<!-- generated by scripts/gstack-patch.sh from gstack upstream. Do not edit by hand:
     changes are lost on the next scripts/gstack-sync.sh run. -->
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs -->


## When to invoke this skill

Navigate any URL, interact with
elements, verify page state, diff before/after actions, take annotated screenshots, check
responsive layouts, test forms and uploads, handle dialogs, and assert element states.
~100ms per command. Use when you need to test a feature, verify a deployment, dogfood a
user flow, or file a bug with evidence. Use when asked to "open in browser", "test the
site", "take a screenshot", or "dogfood this".

## Plan Mode Safe Operations

In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts.

## Completion Status Protocol

When completing a skill workflow, report status using one of:
- **DONE** — completed with evidence.
- **DONE_WITH_CONCERNS** — completed, but list concerns.
- **BLOCKED** — cannot proceed; state blocker and what was tried.
- **NEEDS_CONTEXT** — missing info; state exactly what is needed.

Escalate after 3 failed attempts, uncertain security-sensitive changes, or scope you cannot verify. Format: `STATUS`, `REASON`, `ATTEMPTED`, `RECOMMENDATION`.

# browse: QA Testing & Dogfooding

Persistent headless Chromium. First call auto-starts (~3s), then ~100ms per command.
State persists between calls (cookies, tabs, login sessions).

## Section index — Read each section when its situation applies

This skill is a decision-tree skeleton. The steps below point to on-demand
sections. Read a section in full before doing its step; do not work from memory.

| When | Read this section |
|------|-------------------|
| using any command or snapshot flag beyond the Most-Used Commands table — the full generated reference for every browse command, its argument shape, and every snapshot flag | `sections/command-list.md` |

## SETUP (run this check BEFORE any browse command)

```bash
_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
B=""
[ -n "$_ROOT" ] && [ -x "$_ROOT/.gstack/browse/dist/browse" ] && B="$_ROOT/.gstack/browse/dist/browse"
[ -z "$B" ] && B="$HOME/.gstack/browse/dist/browse"
if [ -x "$B" ]; then
  echo "READY: $B"
else
  echo "NEEDS_SETUP"
fi
```

If `NEEDS_SETUP`:
1. Tell the user: "gstack browse needs a one-time build (~10 seconds). OK to proceed?" Then STOP and wait.
2. Run: `cd <SKILL_DIR> && ./setup`
3. If `bun` is not installed:
   ```bash
   if ! command -v bun >/dev/null 2>&1; then
     BUN_VERSION="1.3.10"
     BUN_INSTALL_SHA="bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd"
     tmpfile=$(mktemp)
     curl -fsSL "https://bun.sh/install" -o "$tmpfile"
     # shasum is macOS/perl; coreutils-only Linux ships sha256sum instead —
     # resolve whichever exists so the verify never fails on a missing tool.
     if command -v sha256sum >/dev/null 2>&1; then
       actual_sha=$(sha256sum "$tmpfile" | awk '{print $1}')
     else
       actual_sha=$(shasum -a 256 "$tmpfile" | awk '{print $1}')
     fi
     if [ "$actual_sha" != "$BUN_INSTALL_SHA" ]; then
       echo "ERROR: bun install script checksum mismatch" >&2
       echo "  expected: $BUN_INSTALL_SHA" >&2
       echo "  got:      $actual_sha" >&2
       rm "$tmpfile"; exit 1
     fi
     BUN_VERSION="$BUN_VERSION" bash "$tmpfile"
     rm "$tmpfile"
   fi
   ```

## Core QA Patterns

### 1. Verify a page loads correctly
```bash
$B goto https://yourapp.com
$B text                          # content loads?
$B console                       # JS errors?
$B network                       # failed requests?
$B is visible ".main-content"    # key elements present?
```

### 2. Test a user flow
```bash
$B goto https://app.com/login
$B snapshot -i                   # see all interactive elements
$B fill @e3 "user@test.com"
$B fill @e4 "password"
$B click @e5                     # submit
$B snapshot -D                   # diff: what changed after submit?
$B is visible ".dashboard"       # success state present?
```

### 3. Verify an action worked
```bash
$B snapshot                      # baseline
$B click @e3                     # do something
$B snapshot -D                   # unified diff shows exactly what changed
```

### 4. Visual evidence for bug reports
```bash
$B snapshot -i -a -o /tmp/annotated.png   # labeled screenshot
$B screenshot /tmp/bug.png                # plain screenshot
$B console                                # error log
```

Two behaviors that silently invalidate screenshots (#2445 — designed, but
surprising):
- **`hover` scrolls its target into view.** Hovering anything below the fold
  scrolls the page first, so a "rest state" shot taken afterwards captures
  the wrong section with exit 0. Before a rest-state screenshot, hover only
  something already visible, and assert position when it matters:
  `$B js "window.scrollY"` should be `0` (or your intended offset).
- **The tab persists across sessions.** The daemon keeps its tab between your
  sessions, so `reload` or `screenshot` without a preceding `goto` can act on
  whatever page earlier work left open. Start verification passes with an
  explicit `$B goto <url>`, never a bare `reload`.

### 5. Find all clickable elements (including non-ARIA)
```bash
$B snapshot -C                   # finds divs with cursor:pointer, onclick, tabindex
$B click @c1                     # interact with them
```

### 6. Assert element states
```bash
$B is visible ".modal"
$B is enabled "#submit-btn"
$B is disabled "#submit-btn"
$B is checked "#agree-checkbox"
$B is editable "#name-field"
$B is focused "#search-input"
$B js "document.body.textContent.includes('Success')"
```

### 7. Test responsive layouts
```bash
$B responsive /tmp/layout        # mobile + tablet + desktop screenshots
$B viewport 375x812              # or set specific viewport
$B screenshot /tmp/mobile.png
```

### 8. Test file uploads
```bash
$B upload "#file-input" /path/to/file.pdf
$B is visible ".upload-success"
```

### 9. Test dialogs
```bash
$B dialog-accept "yes"           # set up handler
$B click "#delete-button"        # trigger dialog
$B dialog                        # see what appeared
$B snapshot -D                   # verify deletion happened
```

### 10. Compare environments
```bash
$B diff https://staging.app.com https://prod.app.com
```

### 11. Show screenshots to the user
After `$B screenshot`, `$B snapshot -a -o`, or `$B responsive`, always use the Read tool on the output PNG(s) so the user can see them. Without this, screenshots are invisible.

### 12. Render local HTML (no HTTP server needed)
Two paths, pick the cleaner one:
```bash
# HTML file on disk → goto file:// (absolute, or cwd-relative)
$B goto file:///tmp/report.html
$B goto file://./docs/page.html        # cwd-relative
$B goto file://~/Documents/page.html   # home-relative

# HTML generated in memory → load-html reads the file into setContent
echo '<div class="tweet">hello</div>' > /tmp/tweet.html
$B load-html /tmp/tweet.html
```

`goto file://...` is usually cleaner (URL is saved in state, relative asset URLs resolve against the file's dir, scale changes replay naturally). `load-html` uses `page.setContent()` — URL stays `about:blank`, but the content survives `viewport --scale` via in-memory replay. Both are scoped to files under cwd or `$TMPDIR`.

### 13. Retina screenshots (deviceScaleFactor)
```bash
$B viewport 480x600 --scale 2       # 2x deviceScaleFactor
$B load-html /tmp/tweet.html        # or: $B goto file://./tweet.html
$B screenshot /tmp/out.png --selector .tweet-card
# → /tmp/out.png is 2x the pixel dimensions of the element
```
Scale must be 1-3 (gstack policy cap). Changing `--scale` recreates the browser context; refs from `snapshot` are invalidated (rerun `snapshot`), but `load-html` content is replayed automatically. Not supported in headed mode.

### 14. Offline render mode (rasterize your own HTML/JSON, zero network)

This is the blessed path for "I just want to turn my own local HTML or JSON into a
PNG/PDF/bytes on disk" — Excalidraw diagrams, tweet/quote cards, og-images,
report rasterization. It is **plain headless, shared Chromium, no proxy, no Xvfb,
no anti-bot stealth**. Default `$B` is already exactly this; you do not pass
`--headed` or `--proxy`. One Chromium per box, shared by every skill — **do not
`npm i puppeteer` and ship a second browser** (see the note under the cheatsheet).

Two output shapes, pick by what you have:

**A) Visual output → `screenshot --selector` (preferred).** If the thing you want
is a picture of something on the page, screenshot it. The PNG is written from the
browser process straight to disk — the image bytes never cross the CDP wire.

```bash
echo '<div id="card" style="width:400px;height:200px;background:#1da1f2;color:#fff;padding:20px">hi</div>' > /tmp/card.html
$B viewport 480x600 --scale 2
$B load-html /tmp/card.html
$B screenshot /tmp/card.png --selector '#card'   # disk path — no megabytes over CDP
```
(Use the disk path, NOT `screenshot --base64` — base64 serializes the bytes back
through the command channel, which is the cost you're trying to avoid.)

**B) Bytes a function returns → `js --out` / `eval --out`.** When a library hands
you the result as a return value (a base64 data URL, a blob, computed JSON) rather
than painting a stable element — e.g. Excalidraw's export function returns a PNG
data URL — write the evaluate result straight to disk. `--out` decodes a
`data:*;base64,...` result to raw bytes automatically (pass `--raw` to write the
literal string). The payload is written by the daemon and never serialized back
out to the CLI/stdout.

```bash
# Load the render bundle, signal readiness, then render-to-file.
$B load-html /tmp/excalidraw-export.html        # bundle sets window.__render + a #done flag
$B wait '#done'                                  # deterministic ready handshake
$B js "window.__render(SCENE_JSON)" --out /tmp/diagram.png   # data URL → decoded PNG on disk
```

`--out` is a WRITE: it needs the `write` scope and is never allowed over the
pair-agent tunnel (a remote agent can't write to your disk). Parent directories
are created; malformed base64 errors instead of writing corrupt bytes. Pick A when
you can (no CDP transfer at all); reach for B only when the bytes come back as a
return value.

## Puppeteer → browse cheatsheet

Migrating from Puppeteer? Here's the 1:1 mapping for the core workflow:

| Puppeteer | browse |
|---|---|
| `await page.goto(url)` | `$B goto <url>` |
| `await page.setContent(html)` | `$B load-html <file>` (or `$B goto file://<abs>`) |
| `await page.setViewport({width, height})` | `$B viewport WxH` |
| `await page.setViewport({width, height, deviceScaleFactor: 2})` | `$B viewport WxH --scale 2` |
| `await (await page.$('.x')).screenshot({path})` | `$B screenshot <path> --selector .x` |
| `await page.screenshot({fullPage: true, path})` | `$B screenshot <path>` (full page default) |
| `await page.screenshot({clip: {x, y, w, h}, path})` | `$B screenshot <path> --clip x,y,w,h` |
| `const r = await page.evaluate(fn)` | `$B js "<expr>"` (result to stdout) |
| `fs.writeFileSync(out, Buffer.from(dataUrl.split(',')[1],'base64'))` | `$B js "<expr>" --out <file>` (data URL auto-decoded) |

Worked example (the tweet-renderer flow — Puppeteer → browse):

```bash
# Generate HTML in memory, render at 2x scale, screenshot the tweet card.
echo '<div class="tweet-card" style="width:400px;height:200px;background:#1da1f2;color:white;padding:20px">hello</div>' > /tmp/tweet.html
$B viewport 480x600 --scale 2
$B load-html /tmp/tweet.html
$B screenshot /tmp/out.png --selector .tweet-card
# /tmp/out.png is 800x400 px, crisp (2x deviceScaleFactor).
```

Aliases: typing `setcontent` or `set-content` routes to `load-html` automatically. Typing a typo (`load-htm`) returns `Did you mean 'load-html'?`.

**Don't bundle your own puppeteer/Chromium.** `browse` is the one shared Chromium
per box. Skills that need to rasterize local HTML/JSON (diagrams, cards, og-images)
should route through `browse` — `screenshot --selector` for visual output,
`load-html` + `js --out` for bytes a function returns — instead of
`npm i puppeteer` and downloading a second Chromium that drifts out of version sync.
One install to pin, one daemon's lifecycle to manage.

## Session Persistence (opt-in)

By default the headless daemon's cookies and tab state die with it — a crash,
version auto-restart, or `browse stop` logs you out of everything (#778).
Opt in to persistence with `BROWSE_PERSIST_STATE=1` in the daemon's
environment: the daemon then snapshots cookies + per-tab
URL/localStorage/sessionStorage to `<stateDir>/session-state.json` (0600)
every 30 seconds and at clean shutdown, and restores it on the next launch.

Facts that matter:
- **Default OFF.** Cookies on disk are a real cost; the user opts in.
- **Headless only.** Headed mode's persistent Chromium profile already owns
  its state; replaying tabs would clobber the user's window.
- **Never persisted:** loaded HTML and tab ownership — a tampered state file
  cannot smuggle content past load-html's checks or forge ownership. Cookies
  for localhost, `.internal`, and cloud-metadata addresses are dropped on
  restore.
- **Corrupt state** is moved to `session-state.json.corrupt` (kept for
  diagnosis) and the daemon boots fresh — persistence can never block a
  launch. The boot log says which happened: `Session state restored: N
  cookies / M tabs` or `fresh session`.

## User Handoff

When you hit something you can't handle in headless mode (CAPTCHA, complex auth, multi-factor
login), hand off to the user:

```bash
# 1. Open a visible Chrome at the current page
$B handoff "Stuck on CAPTCHA at login page"

# 2. Tell the user what happened (via AskUserQuestion)
#    "I've opened Chrome at the login page. Please solve the CAPTCHA
#     and let me know when you're done."

# 3. When user says "done", re-snapshot and continue
$B resume
```

**When to use handoff:**
- CAPTCHAs or bot detection
- Multi-factor authentication (SMS, authenticator app)
- OAuth flows that require user interaction
- Complex interactions the AI can't handle after 3 attempts

The browser preserves all state (cookies, localStorage, tabs) across the handoff.
After `resume`, you get a fresh snapshot of wherever the user left off.

## Headed Mode + Proxy + Anti-Bot Sites

For sites that block headless browsers, fingerprint Playwright defaults, or require routing through an authenticated SOCKS5 proxy (residential VPN, etc.), browse exposes three coordinated flags:

```bash
# Headed mode — visible Chromium window. Auto-spawns Xvfb on Linux
# containers without DISPLAY (no extra setup needed on Debian/Ubuntu).
browse --headed goto https://example.com

# SOCKS5 with auth (Chromium can't prompt for SOCKS5 creds itself —
# browse runs a local 127.0.0.1 bridge that handles the auth handshake).
browse --proxy socks5://user:pass@residential.proxy.host:1080 goto https://example.com

# HTTP/HTTPS proxy (passes through to Chromium directly):
browse --proxy http://corp-proxy:3128 goto https://example.com

# Browser-triggered file download (Content-Disposition, redirect chain,
# anti-bot CDN — falls back from page.request.fetch() to browser native
# download handler):
browse download "https://protected.example.com/file" /tmp/file.bin --navigate

# Combined: headed + proxy + navigate-download
browse --headed --proxy socks5://user:pass@host:1080 \
  download "https://protected.example.com/file" /tmp/file.bin --navigate
```

**Credential policy.** Pass creds via either the URL (`socks5://user:pass@host`) OR the env vars `BROWSE_PROXY_USER` and `BROWSE_PROXY_PASS` — never both. Browse refuses with a clear hint when both are set, because silent override creates "works on my machine" debugging traps.

**Daemon discipline.** Browse runs as a long-lived daemon. `--proxy` and `--headed` change daemon-startup config, so they only apply on a fresh daemon. If a daemon is already running with different config, browse refuses and tells you to `browse disconnect` first. No silent restart that would drop tab state, cookies, or logged-in sessions.

**Stealth.** When `--headed` or `--proxy` are set, browse masks `navigator.webdriver` (the obvious automation tell) via Chromium's `--disable-blink-features=AutomationControlled` plus a small init script. We do NOT fake `navigator.plugins`, `navigator.languages`, or `window.chrome` — modern fingerprinters check those for consistency, and synthesizing fixed values can flag MORE bot-like, not less.

**Container support.** `--headed` on Linux without `DISPLAY` automatically picks a free X display (`:99`, `:100`, ...) and spawns Xvfb. Cleanup on `browse disconnect` validates the recorded PID's `/proc/<pid>/cmdline` matches `Xvfb` AND start-time matches before sending any signal — no PID-reuse footguns. Standard Debian/Ubuntu containers work out of the box; minimal images (alpine, distroless) may also need fonts/dbus/gtk libs for headed Chromium to render.

**Failure modes.** SOCKS5 upstream rejected or unreachable → fail-fast at startup with a redacted error after 3 retries (5s budget). Mid-stream upstream drop → browse kills the affected client connection only; no transport retries (which could corrupt browser traffic). Mismatched daemon config → exit 1 with a `browse disconnect` hint.

## CSS Inspector & Style Modification

### Inspect element CSS
```bash
$B inspect .header              # full CSS cascade for selector
$B inspect                      # latest picked element from sidebar
$B inspect --all                # include user-agent stylesheet rules
$B inspect --history            # show modification history
```

### Modify styles live
```bash
$B style .header background-color #1a1a1a   # modify CSS property
$B style --undo                              # revert last change
$B style --undo 2                            # revert specific change
```

### Clean screenshots
```bash
$B cleanup --all                 # remove ads, cookies, sticky, social
$B cleanup --ads --cookies       # selective cleanup
$B prettyscreenshot --cleanup --scroll-to ".pricing" --width 1440 ~/Desktop/hero.png
```

## Most-Used Commands

The commands that cover most QA sessions (`$B <command>`):

| Command | What it does |
|---------|--------------|
| `goto <url>` | Navigate (also `file://` paths) |
| `snapshot -i` | Accessibility tree with @e refs for interactive elements (`-D` diff, `-C` cursor-interactive @c refs, `-a -o <png>` annotated shot) |
| `click <sel>` / `fill <sel> <val>` | Interact — CSS selectors or @refs |
| `text` / `html [sel]` | Page text / HTML |
| `js "<expr>"` | Run JavaScript, result to stdout |
| `is <state> <sel>` | Assert visible/hidden/enabled/disabled/checked/editable/focused |
| `console` / `network` | JS errors / failed requests |
| `screenshot <path>` | Full-page PNG (`--selector <sel>` for one element) |
| `wait <sel>` | Wait for element (max 10s) |
| `viewport WxH` | Set viewport (`--scale 2` for retina) |

Everything else (extraction, tabs, dialogs, uploads, meta/server commands, and the
full snapshot-flag reference) lives in the generated section below — read it before
reaching for a command that is not in this table.

> **STOP.** Before using any command or snapshot flag beyond the Most-Used Commands table — the full generated reference for every browse command, its argument shape, and every snapshot flag, Read `~/.gstack/browse/sections/command-list.md` and execute it
> in full. Do not work from memory — that section is the source of truth for this step.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.