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.
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 runningChrome starts with --remote-debugging-port=9222 and separate profile in /tmp/chrome-debug (or C:\temp\chrome-debug on Windows).
| Variable | Default | Description |
|---|---|---|
CHROME_WS_BROWSER |
(auto-detect) | Path to browser executable. Overrides auto-detection. |
CHROME_WS_HOST |
127.0.0.1 |
Debug host address |
CHROME_WS_PORT |
9222 |
Debug port number |
Examples:
# Force Chromium instead of Chrome
CHROME_WS_BROWSER=/usr/bin/chromium ./chrome-ws start
# Use custom port
CHROME_WS_PORT=9333 ./chrome-ws start
# Use Brave browser
CHROME_WS_BROWSER="/usr/bin/brave-browser" ./chrome-ws startSetup:
chrome-ws start # Launch Chrome (auto-detects platform)Tab Management:
chrome-ws tabs # List tabs
chrome-ws new <url> # Create tab
chrome-ws close <ws-url> # Close tabNavigation:
chrome-ws navigate <tab> <url> # Navigate
chrome-ws wait-for <tab> <selector> # Wait for element
chrome-ws wait-text <tab> <text> # Wait for textInteraction:
chrome-ws click <tab> <selector> # Click
chrome-ws fill <tab> <selector> <value> # Fill input
chrome-ws select <tab> <selector> <value> # Select dropdownExtraction:
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 HTMLExport:
chrome-ws screenshot <tab> <file.png> # Capture screenshot
chrome-ws markdown <tab> <file.md> # Save as markdownRaw Protocol:
chrome-ws raw <ws-url> <json-rpc> # Direct CDP access<tab> accepts either tab index (0, 1, 2) or full WebSocket URL.
Extract page content:
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:
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:
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())
)
")Simple login:
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]" "user@example.com"
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:
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]" "john@example.com"
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:
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"Article content:
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
EOFProduct information:
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"
}
EOFBatch process URLs:
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
doneEmail extraction:
# 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:
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:
# 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"Wait for AJAX to complete:
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:
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:
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
doneMulti-step workflow:
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]" "jane@example.com"
# 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:
# 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:
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:
# 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:
# 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.pngCheck element exists:
# 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"
fiVerify command success:
if ! chrome-ws navigate 0 "https://example.com"; then
echo "Navigation failed - Chrome not running?"
exit 1
fiRetry pattern:
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
doneAlways wait before interaction:
# 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:
# 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:
# Check page structure
chrome-ws html 0 | grep "submit"
# Check specific element exists
chrome-ws html 0 "form"Escape special characters:
# 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'Don't cache tab indices - they change when tabs close:
# 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:
# 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:
# 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"
fiConnection 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.
Full CDP documentation: https://chromedevtools.github.io/devtools-protocol/
Common methods via raw command:
Page.navigateRuntime.evaluateNetwork.enablePerformance.getMetrics