Back to skills
SkillHub ClubWrite Technical DocsFull StackTech WriterIntegration

browsing

Use when you need direct browser control - teaches Chrome DevTools Protocol for controlling existing browser sessions, multi-tab management, form automation, and content extraction via use_browser MCP tool

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
212
Hot score
97
Updated
March 20, 2026
Overall rating
C4.6
Composite score
4.6
Best-practice grade
B75.6

Install command

npx @skill-hub/cli install obra-superpowers-chrome-browsing

Repository

obra/superpowers-chrome

Skill path: skills/browsing

Use when you need direct browser control - teaches Chrome DevTools Protocol for controlling existing browser sessions, multi-tab management, form automation, and content extraction via use_browser MCP tool

Open repository

Best for

Primary workflow: Write Technical Docs.

Technical facets: Full Stack, Tech Writer, Integration.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: obra.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install browsing into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/obra/superpowers-chrome before adding browsing to shared team environments
  • Use browsing for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: browsing
description: Use when you need direct browser control - teaches Chrome DevTools Protocol for controlling existing browser sessions, multi-tab management, form automation, and content extraction via use_browser MCP tool
allowed-tools: mcp__chrome__use_browser
---

# Browsing with Chrome Direct

## Overview

Control Chrome via DevTools Protocol using the `use_browser` MCP tool. Single unified interface with auto-starting Chrome.

**Announce:** "I'm using the browsing skill to control Chrome."

## When to Use

**Use this when:**
- Controlling authenticated sessions
- Managing multiple tabs in running browser
- Playwright MCP unavailable or excessive

**Use Playwright MCP when:**
- Need fresh browser instances
- Generating screenshots/PDFs
- Prefer higher-level abstractions

## The use_browser Tool

Single MCP tool with action-based interface. Chrome auto-starts on first use.

**Parameters:**
- `action` (required): Operation to perform
- `tab_index` (optional): Tab to operate on (default: 0)
- `selector` (optional): CSS selector for element operations
- `payload` (optional): Action-specific data
- `timeout` (optional): Timeout in ms for await operations (default: 5000)

## Actions Reference

### Navigation
- **navigate**: Navigate to URL
  - `payload`: URL string
  - Example: `{action: "navigate", payload: "https://example.com"}`

- **await_element**: Wait for element to appear
  - `selector`: CSS selector
  - `timeout`: Max wait time in ms
  - Example: `{action: "await_element", selector: ".loaded", timeout: 10000}`

- **await_text**: Wait for text to appear
  - `payload`: Text to wait for
  - Example: `{action: "await_text", payload: "Welcome"}`

### Interaction
- **click**: Click element
  - `selector`: CSS selector
  - Example: `{action: "click", selector: "button.submit"}`

- **type**: Type text into input (append `\n` to submit)
  - `selector`: CSS selector
  - `payload`: Text to type
  - Example: `{action: "type", selector: "#email", payload: "[email protected]\n"}`

- **select**: Select dropdown option
  - `selector`: CSS selector
  - `payload`: Option value(s)
  - Example: `{action: "select", selector: "select[name=state]", payload: "CA"}`

### Extraction
- **extract**: Get page content
  - `payload`: Format ('markdown'|'text'|'html')
  - `selector`: Optional - limit to element
  - Example: `{action: "extract", payload: "markdown"}`
  - Example: `{action: "extract", payload: "text", selector: "h1"}`

- **attr**: Get element attribute
  - `selector`: CSS selector
  - `payload`: Attribute name
  - Example: `{action: "attr", selector: "a.download", payload: "href"}`

- **eval**: Execute JavaScript
  - `payload`: JavaScript code
  - Example: `{action: "eval", payload: "document.title"}`

### Export
- **screenshot**: Capture screenshot
  - `payload`: Filename
  - `selector`: Optional - screenshot specific element
  - Example: `{action: "screenshot", payload: "/tmp/page.png"}`

### Tab Management
- **list_tabs**: List all open tabs
  - Example: `{action: "list_tabs"}`

- **new_tab**: Create new tab
  - Example: `{action: "new_tab"}`

- **close_tab**: Close tab
  - `tab_index`: Tab to close
  - Example: `{action: "close_tab", tab_index: 2}`

### Browser Mode Control
- **show_browser**: Make browser window visible (headed mode)
  - Example: `{action: "show_browser"}`
  - ⚠️ **WARNING**: Restarts Chrome, reloads pages via GET, loses POST state

- **hide_browser**: Switch to headless mode (invisible browser)
  - Example: `{action: "hide_browser"}`
  - ⚠️ **WARNING**: Restarts Chrome, reloads pages via GET, loses POST state

- **browser_mode**: Check current browser mode and profile
  - Example: `{action: "browser_mode"}`
  - Returns: `{"headless": true|false, "mode": "headless"|"headed", "running": true|false, "profile": "name", "profileDir": "/path"}`

### Profile Management
- **set_profile**: Change Chrome profile (must kill Chrome first)
  - Example: `{action: "set_profile", "payload": "browser-user"}`
  - ⚠️ **WARNING**: Chrome must be stopped first

- **get_profile**: Get current profile name and directory
  - Example: `{action: "get_profile"}`
  - Returns: `{"profile": "name", "profileDir": "/path"}`

**Default behavior**: Chrome starts in **headless mode** with **"superpowers-chrome" profile**.

**Critical caveats when toggling modes**:
1. **Chrome must restart** - Cannot switch headless/headed mode on running Chrome
2. **Pages reload via GET** - All open tabs are reopened with GET requests
3. **POST state is lost** - Form submissions, POST results, and POST-based navigation will be lost
4. **Session state is lost** - Any client-side state (JavaScript variables, etc.) is cleared
5. **Cookies/auth may persist** - Uses same user data directory, so logged-in sessions may survive

**When to use headed mode**:
- Debugging visual rendering issues
- Demonstrating browser behavior to user
- Testing features that only work with visible browser
- Debugging issues that don't reproduce in headless mode

**When to stay in headless mode** (default):
- All other cases - faster, cleaner, less intrusive
- Screenshots work perfectly in headless mode
- Most automation works identically in both modes

**Profile management**:
Profiles store persistent browser data (cookies, localStorage, extensions, auth sessions).

**Profile locations**:
- macOS: `~/Library/Caches/superpowers/browser-profiles/{name}/`
- Linux: `~/.cache/superpowers/browser-profiles/{name}/`
- Windows: `%LOCALAPPDATA%/superpowers/browser-profiles/{name}/`

**When to use separate profiles**:
- **Default profile ("superpowers-chrome")**: General automation, shared sessions
- **Agent-specific profiles**: Isolate different agents' browser state
  - Example: browser-user agent uses "browser-user" profile
- **Task-specific profiles**: Testing with different user contexts
  - Example: "test-logged-in" vs "test-logged-out"

**Profile data persists across**:
- Chrome restarts
- Mode toggles (headless ↔ headed)
- System reboots (data is in cache directory)

**To use a different profile**:
1. Kill Chrome if running: `await chromeLib.killChrome()`
2. Set profile: `{action: "set_profile", "payload": "my-profile"}`
3. Start Chrome: Next navigate/action will use new profile

## Quick Start Pattern

```
Navigate and extract:
{action: "navigate", payload: "https://example.com"}
{action: "await_element", selector: "h1"}
{action: "extract", payload: "text", selector: "h1"}
```

## Common Patterns

### Fill and Submit Form
```
{action: "navigate", payload: "https://example.com/login"}
{action: "await_element", selector: "input[name=email]"}
{action: "type", selector: "input[name=email]", payload: "[email protected]"}
{action: "type", selector: "input[name=password]", payload: "pass123\n"}
{action: "await_text", payload: "Welcome"}
```

The `\n` at the end of the password submits the form.

### Multi-Tab Workflow
```
{action: "list_tabs"}
{action: "click", tab_index: 2, selector: "a.email"}
{action: "await_element", tab_index: 2, selector: ".content"}
{action: "extract", tab_index: 2, payload: "text", selector: ".amount"}
```

### Dynamic Content
```
{action: "navigate", payload: "https://example.com"}
{action: "type", selector: "input[name=q]", payload: "query"}
{action: "click", selector: "button.search"}
{action: "await_element", selector: ".results"}
{action: "extract", payload: "text", selector: ".result-title"}
```

### Get Link Attribute
```
{action: "navigate", payload: "https://example.com"}
{action: "await_element", selector: "a.download"}
{action: "attr", selector: "a.download", payload: "href"}
```

### Execute JavaScript
```
{action: "eval", payload: "document.querySelectorAll('a').length"}
{action: "eval", payload: "Array.from(document.querySelectorAll('a')).map(a => a.href)"}
```

## Tips

**Always wait before interaction:**
Don't click or fill immediately after navigate - pages need time to load.

```
// BAD - might fail if page slow
{action: "navigate", payload: "https://example.com"}
{action: "click", selector: "button"}  // May fail!

// GOOD - wait first
{action: "navigate", payload: "https://example.com"}
{action: "await_element", selector: "button"}
{action: "click", selector: "button"}
```

**Use specific selectors:**
Avoid generic selectors that match multiple elements.

```
// BAD - matches first button
{action: "click", selector: "button"}

// GOOD - specific
{action: "click", selector: "button[type=submit]"}
{action: "click", selector: "#login-button"}
```

**Submit forms with \n:**
Append newline to text to submit forms automatically.

```
{action: "type", selector: "#search", payload: "query\n"}
```

**Check content first:**
Extract page content to verify selectors before building workflow.

```
{action: "extract", payload: "html"}
```

## Troubleshooting

**Element not found:**
- Use `await_element` before interaction
- Verify selector with `extract` action using 'html' format

**Timeout errors:**
- Increase timeout: `{timeout: 30000}` for slow pages
- Wait for specific element instead of text

**Tab index out of range:**
- Use `list_tabs` to get current indices
- Tab indices change when tabs close

**eval returns `[object Object]`:**
- Use `JSON.stringify()` for complex objects: `{action: "eval", payload: "JSON.stringify({name: 'test'})"}`
- For async functions: `{action: "eval", payload: "JSON.stringify(await yourAsyncFunction())"}`

## Test Automation (Advanced)

<details>
<summary>Click to expand test automation guidance</summary>

When building test automation, you have two approaches:

### Approach 1: use_browser MCP (Simple Tests)
Best for: Single-step tests, direct Claude control during conversation

```json
{"action": "navigate", "payload": "https://app.com"}
{"action": "click", "selector": "#test-button"}
{"action": "eval", "payload": "JSON.stringify({passed: document.querySelector('.success') !== null})"}
```

### Approach 2: chrome-ws CLI (Complex Tests)
Best for: Multi-step test suites, standalone automation scripts

**Key insight**: `chrome-ws` is the reference implementation showing proper Chrome DevTools Protocol usage. When `use_browser` doesn't work as expected, examine how `chrome-ws` handles the same operation.

```bash
# Example: Automated form testing
./chrome-ws navigate 0 "https://app.com/form"
./chrome-ws fill 0 "#email" "[email protected]"
./chrome-ws click 0 "button[type=submit]"
./chrome-ws wait-text 0 "Success"
```

### When use_browser Fails
1. **Check chrome-ws source code** - It shows the correct CDP pattern
2. **Use chrome-ws to verify** - Test the same operation via CLI
3. **Adapt the pattern** - Apply the working CDP approach to use_browser

### Common Test Automation Patterns
- **Form validation**: Fill forms, check error states
- **UI state testing**: Click elements, verify DOM changes
- **Performance testing**: Measure load times, capture metrics
- **Screenshot comparison**: Capture before/after states

</details>

## Advanced Usage

For command-line usage outside Claude Code, see [COMMANDLINE-USAGE.md](COMMANDLINE-USAGE.md).

For detailed examples, see [EXAMPLES.md](EXAMPLES.md).

## Protocol Reference

Full CDP documentation: https://chromedevtools.github.io/devtools-protocol/


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### COMMANDLINE-USAGE.md

```markdown
# Command-Line Usage: chrome-ws Tool

Direct command-line access to Chrome DevTools Protocol via the `chrome-ws` bash tool.

**Note**: For use within Claude Code, the MCP `use_browser` tool is recommended. This document is for direct command-line usage or integration with other tools.

## Setup

```bash
cd ~/.claude/plugins/cache/using-chrome-directly/skills/using-chrome-directly
chmod +x chrome-ws
./chrome-ws start    # Auto-detects platform, launches Chrome
./chrome-ws tabs     # Verify running
```

Chrome starts with `--remote-debugging-port=9222` and separate profile in `/tmp/chrome-debug` (or `C:\temp\chrome-debug` on Windows).

## Command Reference

**Setup:**
```bash
chrome-ws start                 # Launch Chrome (auto-detects platform)
```

**Tab Management:**
```bash
chrome-ws tabs                  # List tabs
chrome-ws new <url>            # Create tab
chrome-ws close <ws-url>       # Close tab
```

**Navigation:**
```bash
chrome-ws navigate <tab> <url>        # Navigate
chrome-ws wait-for <tab> <selector>   # Wait for element
chrome-ws wait-text <tab> <text>      # Wait for text
```

**Interaction:**
```bash
chrome-ws click <tab> <selector>              # Click
chrome-ws fill <tab> <selector> <value>       # Fill input
chrome-ws select <tab> <selector> <value>     # Select dropdown
```

**Extraction:**
```bash
chrome-ws eval <tab> <js>               # Execute JavaScript
chrome-ws extract <tab> <selector>      # Get text content
chrome-ws attr <tab> <selector> <attr>  # Get attribute
chrome-ws html <tab> [selector]         # Get HTML
```

**Export:**
```bash
chrome-ws screenshot <tab> <file.png>   # Capture screenshot
chrome-ws markdown <tab> <file.md>      # Save as markdown
```

**Raw Protocol:**
```bash
chrome-ws raw <ws-url> <json-rpc>       # Direct CDP access
```

`<tab>` accepts either tab index (0, 1, 2) or full WebSocket URL.

## Examples

### Basic Operations

**Extract page content:**
```bash
chrome-ws navigate 0 "https://example.com"
chrome-ws wait-for 0 "h1"

# Get page title
TITLE=$(chrome-ws eval 0 "document.title")

# Get main heading
HEADING=$(chrome-ws extract 0 "h1")

# Get first link URL
LINK=$(chrome-ws attr 0 "a" "href")
```

**Get all links:**
```bash
chrome-ws navigate 0 "https://example.com"
LINKS=$(chrome-ws eval 0 "Array.from(document.querySelectorAll('a')).map(a => ({
  text: a.textContent.trim(),
  href: a.href
}))")
echo "$LINKS"
```

**Extract table data:**
```bash
chrome-ws navigate 0 "https://example.com/data"
chrome-ws wait-for 0 "table"

# Convert table to JSON array
TABLE=$(chrome-ws eval 0 "
  Array.from(document.querySelectorAll('table tr')).map(row =>
    Array.from(row.cells).map(cell => cell.textContent.trim())
  )
")
```

### Form Automation

**Simple login:**
```bash
chrome-ws navigate 0 "https://app.example.com/login"
chrome-ws wait-for 0 "input[name=email]"

# Fill credentials
chrome-ws fill 0 "input[name=email]" "[email protected]"
chrome-ws fill 0 "input[name=password]" "securepass123"

# Submit and wait for dashboard
chrome-ws click 0 "button[type=submit]"
chrome-ws wait-text 0 "Dashboard"
```

**Multi-step form:**
```bash
chrome-ws navigate 0 "https://example.com/register"

# Step 1: Personal information
chrome-ws fill 0 "input[name=firstName]" "John"
chrome-ws fill 0 "input[name=lastName]" "Doe"
chrome-ws fill 0 "input[name=email]" "[email protected]"
chrome-ws click 0 "button.next"

# Wait for step 2 to load
chrome-ws wait-for 0 "input[name=address]"

# Step 2: Address
chrome-ws fill 0 "input[name=address]" "123 Main St"
chrome-ws select 0 "select[name=state]" "IL"
chrome-ws fill 0 "input[name=zip]" "62701"
chrome-ws click 0 "button.submit"

chrome-ws wait-text 0 "Registration complete"
```

**Search with filters:**
```bash
chrome-ws navigate 0 "https://library.example.com/search"
chrome-ws wait-for 0 "form"

# Select category dropdown
chrome-ws select 0 "select[name=category]" "books"

# Fill search term
chrome-ws fill 0 "input[name=query]" "chrome devtools"

# Submit search
chrome-ws click 0 "button[type=submit]"
chrome-ws wait-for 0 ".results"

# Count results
RESULTS=$(chrome-ws eval 0 "document.querySelectorAll('.result').length")
echo "Found $RESULTS results"
```

### Web Scraping

**Article content:**
```bash
chrome-ws navigate 0 "https://blog.example.com/article"
chrome-ws wait-for 0 "article"

# Extract metadata
TITLE=$(chrome-ws extract 0 "article h1")
AUTHOR=$(chrome-ws extract 0 ".author-name")
DATE=$(chrome-ws extract 0 "time")
CONTENT=$(chrome-ws extract 0 "article .content")

# Save to file
cat > article.txt <<EOF
Title: $TITLE
Author: $AUTHOR
Date: $DATE

$CONTENT
EOF
```

**Product information:**
```bash
chrome-ws navigate 0 "https://shop.example.com/product/123"
chrome-ws wait-for 0 ".product-details"

NAME=$(chrome-ws extract 0 "h1.product-name")
PRICE=$(chrome-ws extract 0 ".price")
IMAGE=$(chrome-ws attr 0 ".product-image img" "src")
STOCK=$(chrome-ws extract 0 ".stock-status")

# Output as JSON
cat <<EOF
{
  "name": "$NAME",
  "price": "$PRICE",
  "image": "$IMAGE",
  "in_stock": "$STOCK"
}
EOF
```

**Batch process URLs:**
```bash
URLS=("page1" "page2" "page3")

for URL in "${URLS[@]}"; do
  chrome-ws navigate 0 "https://example.com/$URL"
  chrome-ws wait-for 0 "h1"
  TITLE=$(chrome-ws extract 0 "h1")
  echo "$URL: $TITLE" >> results.txt
done
```

### Multi-Tab Workflows

**Email extraction:**
```bash
# List all tabs
chrome-ws tabs

# Use the email tab index from output (e.g., tab 2)
EMAIL_TAB=2

# Click specific email
chrome-ws click $EMAIL_TAB "a[title*='Organization receipt']"

# Wait for email to load
chrome-ws wait-for $EMAIL_TAB ".email-body"

# Extract donation amount
AMOUNT=$(chrome-ws extract $EMAIL_TAB ".donation-amount")
echo "Donation: $AMOUNT"
```

**Price comparison:**
```bash
chrome-ws navigate 0 "https://store1.com/product"
chrome-ws new "https://store2.com/product"
chrome-ws new "https://store3.com/product"
sleep 3  # Let pages load

PRICE1=$(chrome-ws extract 0 ".price")
PRICE2=$(chrome-ws extract 1 ".price")
PRICE3=$(chrome-ws extract 2 ".price")

echo "Store 1: $PRICE1"
echo "Store 2: $PRICE2"
echo "Store 3: $PRICE3"
```

**Cross-reference between sites:**
```bash
# Get phone number from company site
chrome-ws navigate 0 "https://company.com/contact"
chrome-ws wait-for 0 ".phone"
PHONE=$(chrome-ws extract 0 ".phone")

# Look up phone number in verification site
chrome-ws new "https://lookup.com"
chrome-ws fill 1 "input[name=phone]" "$PHONE"
chrome-ws click 1 "button.search"
chrome-ws wait-for 1 ".results"
chrome-ws extract 1 ".verification-status"
```

### Dynamic Content

**Wait for AJAX to complete:**
```bash
chrome-ws navigate 0 "https://app.com/dashboard"

# Wait for spinner to disappear
chrome-ws eval 0 "new Promise(resolve => {
  const check = () => {
    if (!document.querySelector('.spinner')) {
      resolve(true);
    } else {
      setTimeout(check, 100);
    }
  };
  check();
})"

# Now safe to extract
chrome-ws extract 0 ".dashboard-data"
```

**Infinite scroll:**
```bash
chrome-ws navigate 0 "https://example.com/feed"
chrome-ws wait-for 0 ".feed-item"

# Scroll 5 times
for i in {1..5}; do
  chrome-ws eval 0 "window.scrollTo(0, document.body.scrollHeight)"
  sleep 2
done

# Count loaded items
chrome-ws eval 0 "document.querySelectorAll('.feed-item').length"
```

**Monitor for changes:**
```bash
chrome-ws navigate 0 "https://example.com/status"
END=$(($(date +%s) + 300))

while [ $(date +%s) -lt $END ]; do
  STATUS=$(chrome-ws extract 0 ".status")
  echo "[$(date +%H:%M:%S)] $STATUS"

  if [[ "$STATUS" == *"ERROR"* ]]; then
    echo "ALERT: Error detected"
    break
  fi

  sleep 10
done
```

### Advanced Patterns

**Multi-step workflow:**
```bash
chrome-ws navigate 0 "https://booking.example.com"

# Search
chrome-ws fill 0 "input[name=destination]" "San Francisco"
chrome-ws fill 0 "input[name=checkin]" "2025-12-01"
chrome-ws click 0 "button.search"

# Select hotel
chrome-ws wait-for 0 ".hotel-results"
chrome-ws click 0 ".hotel-card:first-child .select"

# Choose room
chrome-ws wait-for 0 ".room-options"
chrome-ws click 0 ".room[data-type=deluxe] .book"

# Fill guest info
chrome-ws wait-for 0 "form.guest-info"
chrome-ws fill 0 "input[name=firstName]" "Jane"
chrome-ws fill 0 "input[name=lastName]" "Smith"
chrome-ws fill 0 "input[name=email]" "[email protected]"

# Review
chrome-ws click 0 "button.review"
chrome-ws wait-for 0 ".summary"

# Extract confirmation
HOTEL=$(chrome-ws extract 0 ".hotel-name")
TOTAL=$(chrome-ws extract 0 ".total-price")
echo "$HOTEL: $TOTAL"
```

**Cookies and localStorage:**
```bash
# Get cookies
chrome-ws eval 0 "document.cookie"

# Set cookie
chrome-ws eval 0 "document.cookie = 'theme=dark; path=/'"

# Get localStorage
chrome-ws eval 0 "JSON.stringify(localStorage)"

# Set localStorage
chrome-ws eval 0 "localStorage.setItem('lastVisit', new Date().toISOString())"
```

**Handle modals:**
```bash
chrome-ws click 0 "button.open-modal"
chrome-ws wait-for 0 ".modal.visible"

# Fill modal form
chrome-ws fill 0 ".modal input[name=username]" "testuser"
chrome-ws click 0 ".modal button.submit"

# Wait for modal to close
chrome-ws eval 0 "new Promise(resolve => {
  const check = () => {
    if (!document.querySelector('.modal.visible')) {
      resolve(true);
    } else {
      setTimeout(check, 100);
    }
  };
  check();
})"
```

**Network monitoring with raw CDP:**
```bash
# Enable network monitoring
chrome-ws raw 0 '{"id":1,"method":"Network.enable","params":{}}'

# Navigate and capture traffic
chrome-ws navigate 0 "https://api.example.com"

# Get performance metrics
chrome-ws raw 0 '{"id":2,"method":"Performance.getMetrics","params":{}}'
```

**Screenshots and PDF:**
```bash
# Capture screenshot
chrome-ws screenshot 0 "page.png"

# Or use raw CDP for more control
SCREENSHOT=$(chrome-ws raw 0 '{
  "id":1,
  "method":"Page.captureScreenshot",
  "params":{"format":"png","quality":80}
}')

# Extract base64 and save
echo "$SCREENSHOT" | node -pe "JSON.parse(require('fs').readFileSync(0)).result.data" | base64 -d > screenshot.png
```

## Error Handling

**Check element exists:**
```bash
# Verify button exists
EXISTS=$(chrome-ws eval 0 "!!document.querySelector('.important-button')")

if [ "$EXISTS" = "true" ]; then
  chrome-ws click 0 ".important-button"
else
  echo "Button not found on page"
fi
```

**Verify command success:**
```bash
if ! chrome-ws navigate 0 "https://example.com"; then
  echo "Navigation failed - Chrome not running?"
  exit 1
fi
```

**Retry pattern:**
```bash
for attempt in {1..3}; do
  if chrome-ws click 0 ".submit-button"; then
    echo "Click succeeded"
    break
  fi
  echo "Attempt $attempt failed, retrying..."
  sleep 2
done
```

## Best Practices

**Always wait before interaction:**
```bash
# BAD - might fail if page slow to load
chrome-ws navigate 0 "https://example.com"
chrome-ws click 0 "button"  # May fail!

# GOOD - wait for element first
chrome-ws navigate 0 "https://example.com"
chrome-ws wait-for 0 "button"
chrome-ws click 0 "button"
```

**Use specific selectors:**
```bash
# BAD - matches first button on page
chrome-ws click 0 "button"

# GOOD - specific selector
chrome-ws click 0 "button[type=submit]"
chrome-ws click 0 "button.login-button"
chrome-ws click 0 "#submit-form"
```

**Test selectors with html command:**
```bash
# Check page structure
chrome-ws html 0 | grep "submit"

# Check specific element exists
chrome-ws html 0 "form"
```

**Escape special characters:**
```bash
# Use double quotes for variables
chrome-ws fill 0 "input[name=search]" "$SEARCH_TERM"

# Use single quotes for literal strings with special chars
chrome-ws eval 0 'document.querySelector(".item").textContent'
```

## Common Pitfalls

**Don't cache tab indices** - they change when tabs close:
```bash
# BAD - index might be stale
TAB=2
# ... much later ...
chrome-ws click $TAB "button"  # Tab 2 might not exist anymore

# GOOD - fetch fresh before use
chrome-ws tabs
chrome-ws click 2 "button"
```

**Don't forget to wait for dynamic content:**
```bash
# BAD - tries to extract before content loads
chrome-ws navigate 0 "https://app.com"
chrome-ws extract 0 ".user-name"  # Might be empty!

# GOOD - wait for content
chrome-ws navigate 0 "https://app.com"
chrome-ws wait-for 0 ".user-name"
chrome-ws extract 0 ".user-name"
```

**Handle element state:**
```bash
# Check if button is disabled
DISABLED=$(chrome-ws eval 0 "document.querySelector('button.submit').disabled")

if [ "$DISABLED" = "false" ]; then
  chrome-ws click 0 "button.submit"
else
  echo "Button is disabled"
fi
```

## Troubleshooting

**Connection refused:** Verify Chrome running with `curl http://127.0.0.1:9222/json`

**Element not found:** Check page structure with `chrome-ws html 0`

**Timeout:** Use `wait-for` before interaction. Chrome has 30s timeout.

**Tab index out of range:** Run `chrome-ws tabs` to get current indices.

## Protocol Reference

Full CDP documentation: https://chromedevtools.github.io/devtools-protocol/

Common methods via `raw` command:
- `Page.navigate`
- `Runtime.evaluate`
- `Network.enable`
- `Performance.getMetrics`

```

### EXAMPLES.md

```markdown
# Chrome Direct Access Examples (MCP Tool)

Examples using the `use_browser` MCP tool. For command-line bash examples, see [COMMANDLINE-USAGE.md](COMMANDLINE-USAGE.md).

## Table of Contents

1. [Basic Operations](#basic-operations)
2. [Form Automation](#form-automation)
3. [Web Scraping](#web-scraping)
4. [Multi-Tab Workflows](#multi-tab-workflows)
5. [Dynamic Content](#dynamic-content)
6. [Advanced Patterns](#advanced-patterns)

---

## Basic Operations

### Extract Page Content

Navigate to a page and extract various elements:

```
{action: "navigate", payload: "https://example.com"}
{action: "await_element", selector: "h1"}

// Get page title
{action: "eval", payload: "document.title"}

// Get main heading text
{action: "extract", payload: "text", selector: "h1"}

// Get first link URL
{action: "attr", selector: "a", payload: "href"}
```

### Get All Links

Use JavaScript evaluation to get structured data:

```
{action: "navigate", payload: "https://example.com"}
{action: "eval", payload: "Array.from(document.querySelectorAll('a')).map(a => ({ text: a.textContent.trim(), href: a.href }))"}
```

### Extract Table Data

Convert HTML table to structured data:

```
{action: "navigate", payload: "https://example.com/data"}
{action: "await_element", selector: "table"}

// Convert table to JSON array
{action: "eval", payload: "Array.from(document.querySelectorAll('table tr')).map(row => Array.from(row.cells).map(cell => cell.textContent.trim()))"}
```

### Get Page as Markdown

Extract entire page content in markdown format:

```
{action: "navigate", payload: "https://example.com"}
{action: "await_element", selector: "body"}
{action: "extract", payload: "markdown"}
```

---

## Form Automation

### Simple Login

Navigate, fill credentials, and submit:

```
{action: "navigate", payload: "https://app.example.com/login"}
{action: "await_element", selector: "input[name=email]"}

// Fill credentials
{action: "type", selector: "input[name=email]", payload: "[email protected]"}
{action: "type", selector: "input[name=password]", payload: "securepass123\n"}

// Wait for successful login
{action: "await_text", payload: "Dashboard"}
```

Note: The `\n` at the end of the password submits the form.

### Multi-Step Form

Handle forms that show steps progressively:

```
{action: "navigate", payload: "https://example.com/register"}

// Step 1: Personal information
{action: "type", selector: "input[name=firstName]", payload: "John"}
{action: "type", selector: "input[name=lastName]", payload: "Doe"}
{action: "type", selector: "input[name=email]", payload: "[email protected]"}
{action: "click", selector: "button.next"}

// Wait for step 2 to load
{action: "await_element", selector: "input[name=address]"}

// Step 2: Address
{action: "type", selector: "input[name=address]", payload: "123 Main St"}
{action: "select", selector: "select[name=state]", payload: "IL"}
{action: "type", selector: "input[name=zip]", payload: "62701"}
{action: "click", selector: "button.submit"}

{action: "await_text", payload: "Registration complete"}
```

### Search with Filters

Use dropdowns and text inputs together:

```
{action: "navigate", payload: "https://library.example.com/search"}
{action: "await_element", selector: "form"}

// Select category dropdown
{action: "select", selector: "select[name=category]", payload: "books"}

// Fill search term
{action: "type", selector: "input[name=query]", payload: "chrome devtools"}

// Submit and count results
{action: "click", selector: "button[type=submit]"}
{action: "await_element", selector: ".results"}

// Count results
{action: "eval", payload: "document.querySelectorAll('.result').length"}
```

---

## Web Scraping

### Article Content

Extract article metadata and content:

```
{action: "navigate", payload: "https://blog.example.com/article"}
{action: "await_element", selector: "article"}

// Extract metadata
{action: "extract", payload: "text", selector: "article h1"}
{action: "extract", payload: "text", selector: ".author-name"}
{action: "extract", payload: "text", selector: "time"}
{action: "extract", payload: "text", selector: "article .content"}
```

### Product Information

Scrape product details from e-commerce site:

```
{action: "navigate", payload: "https://shop.example.com/product/123"}
{action: "await_element", selector: ".product-details"}

// Extract product data
{action: "extract", payload: "text", selector: "h1.product-name"}
{action: "extract", payload: "text", selector: ".price"}
{action: "attr", selector: ".product-image img", payload: "src"}
{action: "extract", payload: "text", selector: ".stock-status"}
```

### Batch Extract Structured Data

Get multiple products at once using JavaScript:

```
{action: "navigate", payload: "https://shop.example.com/category/electronics"}
{action: "await_element", selector: ".product-grid"}

// Extract all products as structured data
{action: "eval", payload: `
  Array.from(document.querySelectorAll('.product-card')).map(card => ({
    name: card.querySelector('.product-name').textContent,
    price: card.querySelector('.price').textContent,
    image: card.querySelector('img').src,
    url: card.querySelector('a').href
  }))
`}
```

---

## Multi-Tab Workflows

### Email Extraction

List tabs, then extract data from specific tab:

```
// Find email tab
{action: "list_tabs"}

// Use tab 2 for email (from list_tabs output)
{action: "click", tab_index: 2, selector: "a[title*='Organization receipt']"}
{action: "await_element", tab_index: 2, selector: ".email-body"}

// Extract donation amount
{action: "extract", tab_index: 2, payload: "text", selector: ".donation-amount"}
```

### Price Comparison

Open multiple stores and compare prices:

```
// Navigate first tab
{action: "navigate", payload: "https://store1.com/product"}

// Open additional tabs
{action: "new_tab"}
{action: "navigate", tab_index: 1, payload: "https://store2.com/product"}

{action: "new_tab"}
{action: "navigate", tab_index: 2, payload: "https://store3.com/product"}

// Wait for all to load and extract prices
{action: "await_element", tab_index: 0, selector: ".price"}
{action: "extract", tab_index: 0, payload: "text", selector: ".price"}

{action: "await_element", tab_index: 1, selector: ".price"}
{action: "extract", tab_index: 1, payload: "text", selector: ".price"}

{action: "await_element", tab_index: 2, selector: ".price"}
{action: "extract", tab_index: 2, payload: "text", selector: ".price"}
```

### Cross-Reference Between Sites

Extract data from one site and use in another:

```
// Get phone number from company site
{action: "navigate", payload: "https://company.com/contact"}
{action: "await_element", selector: ".phone"}
{action: "extract", payload: "text", selector: ".phone"}

// Store the result, then open verification site
{action: "new_tab"}
{action: "navigate", tab_index: 1, payload: "https://lookup.com"}
{action: "await_element", tab_index: 1, selector: "input[name=phone]"}

// Fill with extracted phone number
{action: "type", tab_index: 1, selector: "input[name=phone]", payload: "<phone-from-previous-extract>"}
{action: "click", tab_index: 1, selector: "button.search"}
{action: "await_element", tab_index: 1, selector: ".results"}
{action: "extract", tab_index: 1, payload: "text", selector: ".verification-status"}
```

---

## Dynamic Content

### Wait for AJAX to Complete

Wait for loading spinner to disappear:

```
{action: "navigate", payload: "https://app.com/dashboard"}

// Wait for spinner to disappear using custom JavaScript
{action: "eval", payload: `
  new Promise(resolve => {
    const check = () => {
      if (!document.querySelector('.spinner')) {
        resolve(true);
      } else {
        setTimeout(check, 100);
      }
    };
    check();
  })
`}

// Now safe to extract
{action: "extract", payload: "text", selector: ".dashboard-data"}
```

### Infinite Scroll

Scroll to load more content:

```
{action: "navigate", payload: "https://example.com/feed"}
{action: "await_element", selector: ".feed-item"}

// Scroll multiple times
{action: "eval", payload: "window.scrollTo(0, document.body.scrollHeight)"}
{action: "await_element", selector: ".feed-item", timeout: 2000}

{action: "eval", payload: "window.scrollTo(0, document.body.scrollHeight)"}
{action: "await_element", selector: ".feed-item", timeout: 2000}

{action: "eval", payload: "window.scrollTo(0, document.body.scrollHeight)"}
{action: "await_element", selector: ".feed-item", timeout: 2000}

// Count loaded items
{action: "eval", payload: "document.querySelectorAll('.feed-item').length"}
```

### Wait for Element to Become Enabled

Wait for button to be clickable:

```
{action: "click", selector: "button.start"}

// Wait for continue button to enable
{action: "eval", payload: `
  new Promise(resolve => {
    const check = () => {
      const btn = document.querySelector('button.continue');
      if (btn && !btn.disabled) {
        resolve(true);
      } else {
        setTimeout(check, 100);
      }
    };
    check();
  })
`}

{action: "click", selector: "button.continue"}
```

---

## Advanced Patterns

### Multi-Step Workflow

Complete booking flow with validation:

```
{action: "navigate", payload: "https://booking.example.com"}

// Search
{action: "type", selector: "input[name=destination]", payload: "San Francisco"}
{action: "type", selector: "input[name=checkin]", payload: "2025-12-01"}
{action: "click", selector: "button.search"}

// Select hotel
{action: "await_element", selector: ".hotel-results"}
{action: "click", selector: ".hotel-card:first-child .select"}

// Choose room
{action: "await_element", selector: ".room-options"}
{action: "click", selector: ".room[data-type=deluxe] .book"}

// Fill guest info
{action: "await_element", selector: "form.guest-info"}
{action: "type", selector: "input[name=firstName]", payload: "Jane"}
{action: "type", selector: "input[name=lastName]", payload: "Smith"}
{action: "type", selector: "input[name=email]", payload: "[email protected]"}

// Review (don't complete)
{action: "click", selector: "button.review"}
{action: "await_element", selector: ".summary"}

// Extract confirmation details
{action: "extract", payload: "text", selector: ".hotel-name"}
{action: "extract", payload: "text", selector: ".total-price"}
```

### Cookies and LocalStorage

Access browser storage:

```
// Get cookies
{action: "eval", payload: "document.cookie"}

// Set cookie
{action: "eval", payload: "document.cookie = 'theme=dark; path=/'"}

// Get localStorage
{action: "eval", payload: "JSON.stringify(localStorage)"}

// Set localStorage
{action: "eval", payload: "localStorage.setItem('lastVisit', new Date().toISOString())"}
```

### Handle Modals

Interact with modal dialogs:

```
{action: "click", selector: "button.open-modal"}
{action: "await_element", selector: ".modal.visible"}

// Fill modal form
{action: "type", selector: ".modal input[name=username]", payload: "testuser"}
{action: "click", selector: ".modal button.submit"}

// Wait for modal to close
{action: "eval", payload: `
  new Promise(resolve => {
    const check = () => {
      if (!document.querySelector('.modal.visible')) {
        resolve(true);
      } else {
        setTimeout(check, 100);
      }
    };
    check();
  })
`}
```

### Screenshots

Capture full page or specific elements:

```
// Full page screenshot
{action: "navigate", payload: "https://example.com"}
{action: "await_element", selector: "body"}
{action: "screenshot", payload: "/tmp/page.png"}

// Element-specific screenshot
{action: "screenshot", payload: "/tmp/element.png", selector: ".important-section"}
```

### Check Element State

Verify element properties before interaction:

```
// Check if button is disabled
{action: "eval", payload: "document.querySelector('button.submit').disabled"}

// Check if element is visible
{action: "eval", payload: "!!document.querySelector('.important-button') && window.getComputedStyle(document.querySelector('.important-button')).display !== 'none'"}

// Check element exists
{action: "eval", payload: "!!document.querySelector('.important-button')"}
```

---

## Tips and Best Practices

### Always Wait Before Interaction

Don't interact with elements immediately after navigation:

```
// BAD - might fail if page slow to load
{action: "navigate", payload: "https://example.com"}
{action: "click", selector: "button"}  // May fail!

// GOOD - wait for element first
{action: "navigate", payload: "https://example.com"}
{action: "await_element", selector: "button"}
{action: "click", selector: "button"}
```

### Use Specific Selectors

Avoid generic selectors that match multiple elements:

```
// BAD - matches first button on page
{action: "click", selector: "button"}

// GOOD - specific selector
{action: "click", selector: "button[type=submit]"}
{action: "click", selector: "button.login-button"}
{action: "click", selector: "#submit-form"}
```

### Verify Selectors First

Check page structure before building workflow:

```
// Check page HTML
{action: "extract", payload: "html"}

// Or check specific element
{action: "extract", payload: "html", selector: "form"}
```

### Handle Dynamic Content

Wait for content to load before extraction:

```
// BAD - tries to extract before content loads
{action: "navigate", payload: "https://app.com"}
{action: "extract", payload: "text", selector: ".user-name"}  // Might be empty!

// GOOD - wait for content
{action: "navigate", payload: "https://app.com"}
{action: "await_element", selector: ".user-name"}
{action: "extract", payload: "text", selector: ".user-name"}
```

### Use \n for Form Submission

Append newline to auto-submit forms:

```
// Submit search without explicit click
{action: "type", selector: "#search-input", payload: "my query\n"}

// Submit login form
{action: "type", selector: "input[name=email]", payload: "[email protected]"}
{action: "type", selector: "input[name=password]", payload: "password123\n"}
```

---

## Common Pitfalls

### Don't Cache Tab Indices

Tab indices change when tabs close - always get fresh list:

```
// BAD - index might be stale after closing tabs
// (using tab 2 without checking if it still exists)
{action: "click", tab_index: 2, selector: "button"}

// GOOD - list tabs first
{action: "list_tabs"}
// Then use appropriate index from the output
{action: "click", tab_index: 2, selector: "button"}
```

### Increase Timeout for Slow Pages

Default timeout is 5000ms, increase if needed:

```
// For slow-loading elements
{action: "await_element", selector: ".lazy-content", timeout: 30000}

// For slow AJAX requests
{action: "await_text", payload: "Data loaded", timeout: 15000}
```

### Extract Structured Data with JavaScript

For complex data extraction, use JavaScript evaluation:

```
// Instead of multiple extract calls, use one eval
{action: "eval", payload: `
  {
    title: document.querySelector('h1').textContent,
    author: document.querySelector('.author').textContent,
    date: document.querySelector('time').textContent,
    links: Array.from(document.querySelectorAll('a')).map(a => a.href)
  }
`}
```

---

## Reference

- [SKILL.md](SKILL.md) - Complete tool reference
- [COMMANDLINE-USAGE.md](COMMANDLINE-USAGE.md) - Command-line bash examples
- [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) - Full protocol documentation

```

browsing | SkillHub