npx skills add ...
npx skills add the-focus-ai/chrome-driver --skill browser-automation
Automate Chrome browser via DevTools Protocol. Use when user asks to scrape websites, take screenshots, generate PDFs, interact with web pages, extract content, fill forms, or automate browser tasks.
npx skills add the-focus-ai/chrome-driver --skill browser-automation
Control Chrome browser programmatically using simple command-line scripts. All scripts auto-start Chrome in headless mode if not running.
All commands support these options for session persistence and interactive use:
--no-headless - Run Chrome with a visible window (required for interactive sites, login, etc.)--user-data=PATH - Use a specific Chrome profile directory to preserve cookies, logins, and session state between runsExample: Persistent session for logged-in sites
All scripts are in ${CLAUDE_PLUGIN_ROOT}/bin/:
Options:
--full-page - Capture entire scrollable page--selector=CSS - Capture specific element--format=png|jpeg|webp - Output format (default: png)--quality=N - JPEG/WebP quality 0-100--width=N --height=N - Set viewport size--max-dimension=N - Max output dimension (default: 8000, auto-scales large pages)Examples:
Options:
--paper=letter|a4|legal|a3|a5|tabloid - Paper size (default: letter)--landscape - Landscape orientation--margin=INCHES - All margins (default: 0.4)--scale=FACTOR - Scale 0.1-2.0 (default: 1.0)--no-background - Skip background colors/imagesExamples:
Options:
--format=markdown|text|html - Output format (default: markdown)--selector=CSS - Extract specific element only--links - Also list all links--images - Also list all images--metadata - Also show page metadataExamples:
Options:
--wait-for=SELECTOR - Wait for element to appear--click=SELECTOR - Click an element--type=SELECTOR=TEXT - Type text into input field--eval=JAVASCRIPT - Execute JavaScript and print result--timeout=SECONDS - Timeout (default: 30)Examples:
Options:
--fill=SELECTOR=VALUE - Fill input field (can repeat)--select=SELECTOR=VALUE - Select dropdown option (can repeat)--fill-json='{"sel":"val"}' - Fill multiple fields from JSON--submit=SELECTOR - Click submit button after filling--wait-for=SELECTOR - Wait for element before filling--wait-after=SELECTOR - Wait for element after submit--screenshot=PATH - Take screenshot after completionExamples:
Options:
--duration=SECONDS - Recording duration (default: 5)--count=N - Exact number of frames to capture--format=jpeg|png - Frame format (default: jpeg)--quality=N - JPEG quality 0-100 (default: 80)--max-width=N - Maximum frame width--max-height=N - Maximum frame height--fps=N - Approximate frames per second (default: 10)Examples:
This is the key command for multi-step workflows. Unlike navigate, this command connects to the already-open browser tab and performs actions WITHOUT reloading the page. Essential for:
Options:
--click=SELECTOR - Click element matching CSS selector--type=SELECTOR=TEXT - Type text into input field--eval=JAVASCRIPT - Execute JavaScript and print result--wait-for=SELECTOR - Wait for element to appear--select=SELECTOR=VALUE - Select dropdown option--focus=SELECTOR - Focus an element--text=SELECTOR - Get text content of element--timeout=SECONDS - Timeout (default: 30)Examples:
Shows whether Chrome is running, version info, and open tabs.
--no-headless for visible browser)--user-data=PATH to preserve login/cookies between sessionspkill -f 'chrome.*--remote-debugging-port' to stop Chrome manuallyCHROME_DRIVER_PORT to change--help for full usage infoFor sites requiring login (Instacart, Amazon, banking, etc.):
--user-data=~/.chrome-sitename consistently--no-headless to see the browser and log in manually--user-data path preserves your session--user-data paths for different accounts/sitesThis section walks through automating complex multi-step processes on sites that require login and multiple interactions. The key insight is using two commands together:
navigate - Go to a URL (use for initial navigation)interact - Work with the current page without reloading (use for everything after)Why navigate alone isn't enough:
navigate ALWAYS loads/reloads the specified URLnavigate call is independent - it doesn't "continue" from the previous stateWhy interact is essential:
interact connects to the EXISTING browser tabinteract callsHere's a real-world example that works:
First time only - open browser visibly so user can log in:
After login, go to the desired store:
Now use interact to search WITHOUT navigating away:
Check what products are available:
Click the Add button for the desired product:
Confirm the item was added:
When CSS selectors are complex or dynamic, use --eval to find elements:
When --click doesn't work (dynamic elements, overlays, etc.):
Different approaches for form submission:
Use --wait-for or JavaScript polling:
Extract information to make decisions:
Solution: Use interact instead of navigate for all steps after initial navigation.
Solution:
--eval to inspect the page structure--wait-for or sleepSolution:
--eval="document.querySelector('...').click()"Solution:
form.dispatchEvent(new Event('submit', ...))Solution:
--user-data path--user-data for sites requiring login--no-headless during development to see what's happeningnavigate to load the initial pageinteract for all subsequent actions on that page--eval liberally to inspect page state and find elementssleep between actions if the site needs time to updateThis section contains battle-tested patterns for automating Instacart shopping. These patterns were developed through extensive real-world testing.
CRITICAL: Never construct Instacart URLs manually. Always navigate via the UI:
Combined one-liner for efficiency:
List available "Add" buttons (shows product names):
Get page content to see prices and sizes:
Instacart uses aria-labels for product buttons. Key patterns:
| Action | Aria-label Pattern |
|---|---|
| Add item | Add 1 ct [Product Name] or Add 1 lb [Product Name] |
| Increment | Increment quantity of [Product Name] |
| Decrement | Decrement quantity of [Product Name] |
| Remove | Remove [Product Name] |
Add a product (exact match):
Add a product (partial match - more flexible):
Increment quantity (add more of same item):
Increment multiple times (e.g., get 6 avocados):
JavaScript loop for multiple increments (faster):
Remove item from cart:
View cart (click cart badge):
Get cart total:
List all items in cart with prices (when cart panel is open):
Find all remove/decrement buttons in cart:
Efficient pattern for adding multiple items:
Problem: Search terms concatenate (e.g., "lemonsfresh thyme")
input.select(); document.execCommand('delete') before typingProblem: "Add" button not found
JSON.stringify(Array.from(document.querySelectorAll('button[aria-label*="Add"]')).slice(0,8).map(b => b.getAttribute('aria-label')))aria-label*="Add"][aria-label*="partial name"Problem: Store not available at address
Problem: Cart shows wrong item count
Problem: Prices differ between stores
interact after initial navigate - Instacart is an SPA; navigation resets state2>/dev/null - Suppresses noisy stderr output from Chrome driversleep 3 after search submit - Allow time for results to load*=) - More robust than exact matches${CLAUDE_PLUGIN_ROOT}/bin/screenshot URL [OUTPUT] [OPTIONS]# Basic screenshot
${CLAUDE_PLUGIN_ROOT}/bin/screenshot https://example.com /tmp/page.png
# Full page as JPEG
${CLAUDE_PLUGIN_ROOT}/bin/screenshot https://example.com /tmp/full.jpg --full-page --format=jpeg
# Capture specific element
${CLAUDE_PLUGIN_ROOT}/bin/screenshot https://example.com /tmp/header.png --selector="header"${CLAUDE_PLUGIN_ROOT}/bin/pdf URL [OUTPUT] [OPTIONS]# Basic PDF
${CLAUDE_PLUGIN_ROOT}/bin/pdf https://example.com /tmp/doc.pdf
# A4 landscape
${CLAUDE_PLUGIN_ROOT}/bin/pdf https://example.com /tmp/report.pdf --paper=a4 --landscape
# Tight margins
${CLAUDE_PLUGIN_ROOT}/bin/pdf https://example.com /tmp/compact.pdf --margin=0.25${CLAUDE_PLUGIN_ROOT}/bin/extract URL [OPTIONS]# Get page as markdown
${CLAUDE_PLUGIN_ROOT}/bin/extract https://example.com
# Get plain text from article
${CLAUDE_PLUGIN_ROOT}/bin/extract https://example.com --format=text --selector="article"
# Get all links and metadata
${CLAUDE_PLUGIN_ROOT}/bin/extract https://example.com --links --metadata${CLAUDE_PLUGIN_ROOT}/bin/navigate URL [OPTIONS]# Navigate and wait for content
${CLAUDE_PLUGIN_ROOT}/bin/navigate https://example.com --wait-for="#content"
# Fill form and submit
${CLAUDE_PLUGIN_ROOT}/bin/navigate https://google.com --type="input[name=q]=hello" --click="input[type=submit]"
# Get page title via JavaScript
${CLAUDE_PLUGIN_ROOT}/bin/navigate https://example.com --eval="document.title"${CLAUDE_PLUGIN_ROOT}/bin/form URL [OPTIONS]# Login form
${CLAUDE_PLUGIN_ROOT}/bin/form https://example.com/login \
--fill='#username=john' \
--fill='#password=secret' \
--submit='button[type=submit]'
# Form with dropdowns
${CLAUDE_PLUGIN_ROOT}/bin/form https://example.com/register \
--fill='#name=John Doe' \
--fill='#email=john@example.com' \
--select='#country=US' \
--submit='#register-btn'
# Using JSON
${CLAUDE_PLUGIN_ROOT}/bin/form https://example.com/contact \
--fill-json='{"#name":"John","#email":"john@test.com"}' \
--submit='button.send'${CLAUDE_PLUGIN_ROOT}/bin/record URL OUTPUT_DIR [OPTIONS]# Record 5 seconds
${CLAUDE_PLUGIN_ROOT}/bin/record https://example.com /tmp/frames
# Record 30 PNG frames
${CLAUDE_PLUGIN_ROOT}/bin/record https://example.com /tmp/frames --count=30 --format=png
# Convert to video with ffmpeg
ffmpeg -framerate 10 -i /tmp/frames/frame-%04d.jpg -c:v libx264 output.mp4${CLAUDE_PLUGIN_ROOT}/bin/interact [OPTIONS]# Execute JavaScript on current page
${CLAUDE_PLUGIN_ROOT}/bin/interact --eval="document.title" --user-data=~/.chrome-mysite
# Click a button on current page
${CLAUDE_PLUGIN_ROOT}/bin/interact --click="#submit-btn" --user-data=~/.chrome-mysite
# Type into an input without navigating away
${CLAUDE_PLUGIN_ROOT}/bin/interact --type="#search=query" --user-data=~/.chrome-mysite
# Chain actions: focus, type, then click
${CLAUDE_PLUGIN_ROOT}/bin/interact --focus="#search" --type="#search=pork" --click="#search-btn" --user-data=~/.chrome-mysite
# Get text from an element
${CLAUDE_PLUGIN_ROOT}/bin/interact --text="h1.title" --user-data=~/.chrome-mysite${CLAUDE_PLUGIN_ROOT}/bin/chrome-status${CLAUDE_PLUGIN_ROOT}/bin/screenshot https://example.com /tmp/screenshot.png${CLAUDE_PLUGIN_ROOT}/bin/pdf https://example.com /tmp/document.pdf --paper=a4${CLAUDE_PLUGIN_ROOT}/bin/extract https://example.com --format=markdown${CLAUDE_PLUGIN_ROOT}/bin/form https://example.com/login \
--fill='#username=user' \
--fill='#password=pass' \
--submit='button[type=submit]' \
--wait-after='.dashboard'${CLAUDE_PLUGIN_ROOT}/bin/record https://example.com /tmp/frames --duration=10
ffmpeg -framerate 10 -i /tmp/frames/frame-%04d.jpg -c:v libx264 video.mp4# Open Instacart with visible browser and persistent profile
${CLAUDE_PLUGIN_ROOT}/bin/navigate https://www.instacart.com \
--no-headless \
--user-data=~/.chrome-instacart
# USER ACTION: Log in manually in the browser window
# Your cookies/session will be saved to ~/.chrome-instacart# First, find the correct store URL by checking the homepage
${CLAUDE_PLUGIN_ROOT}/bin/navigate https://www.instacart.com/store \
--no-headless \
--user-data=~/.chrome-instacart
# Use JavaScript to find links containing your store name
${CLAUDE_PLUGIN_ROOT}/bin/interact \
--eval="Array.from(document.querySelectorAll('a')).filter(a => a.innerText.includes('Market 32')).map(a => a.href).join('\n')" \
--no-headless \
--user-data=~/.chrome-instacart
# Navigate to the store (use the URL found above)
${CLAUDE_PLUGIN_ROOT}/bin/navigate https://www.instacart.com/store/price-chopper-ny/storefront \
--no-headless \
--user-data=~/.chrome-instacart# Click the search input
${CLAUDE_PLUGIN_ROOT}/bin/interact \
--click="#search-bar-input" \
--no-headless \
--user-data=~/.chrome-instacart
# Type the search query
${CLAUDE_PLUGIN_ROOT}/bin/interact \
--type="#search-bar-input=pork shoulder" \
--no-headless \
--user-data=~/.chrome-instacart
# Submit the search using JavaScript (trigger form submit event)
${CLAUDE_PLUGIN_ROOT}/bin/interact \
--eval="var input = document.querySelector('#search-bar-input'); var form = input.closest('form'); form.dispatchEvent(new Event('submit', {bubbles: true, cancelable: true})); 'submitted'" \
--no-headless \
--user-data=~/.chrome-instacart# Wait a moment for results to load, then check the page content
sleep 2
${CLAUDE_PLUGIN_ROOT}/bin/interact \
--eval="document.body.innerText.substring(0, 2000)" \
--no-headless \
--user-data=~/.chrome-instacart# Find and click the first "Add" button
${CLAUDE_PLUGIN_ROOT}/bin/interact \
--eval="var addBtns = Array.from(document.querySelectorAll('button')).filter(b => b.innerText.trim() === 'Add'); if (addBtns.length > 0) { addBtns[0].click(); 'Added to cart'; } else { 'No Add button found'; }" \
--no-headless \
--user-data=~/.chrome-instacart# Check cart count or page state
${CLAUDE_PLUGIN_ROOT}/bin/interact \
--eval="document.body.innerText.includes('2') ? 'Cart has 2 items' : document.body.innerText.substring(0, 500)" \
--no-headless \
--user-data=~/.chrome-instacart# Find all buttons with specific text
--eval="Array.from(document.querySelectorAll('button')).filter(b => b.innerText.includes('Add')).length"
# Find links by text content
--eval="Array.from(document.querySelectorAll('a')).filter(a => a.innerText.includes('Checkout'))[0].href"
# Get input field IDs/names
--eval="JSON.stringify(Array.from(document.querySelectorAll('input')).map(i => ({id: i.id, name: i.name, placeholder: i.placeholder})))"# Click by finding element with JS
--eval="document.querySelector('.add-to-cart-btn').click(); 'clicked'"
# Click first matching element
--eval="Array.from(document.querySelectorAll('button')).filter(b => b.innerText === 'Add')[0].click(); 'clicked'"# Method 1: Dispatch submit event (most reliable for SPAs)
--eval="document.querySelector('form').dispatchEvent(new Event('submit', {bubbles: true, cancelable: true})); 'submitted'"
# Method 2: Click submit button
--click="button[type=submit]"
# Method 3: Call submit() directly
--eval="document.querySelector('form').submit(); 'submitted'"# Wait for element to appear
--wait-for=".search-results"
# Or use JavaScript with timeout
--eval="new Promise(r => { let i = setInterval(() => { if (document.querySelector('.results')) { clearInterval(i); r('found'); }}, 100); setTimeout(() => { clearInterval(i); r('timeout'); }, 5000); })"# Get current URL
--eval="window.location.href"
# Check if logged in
--eval="document.body.innerText.includes('Sign Out') ? 'logged in' : 'not logged in'"
# Get cart count
--eval="document.querySelector('.cart-count')?.innerText || '0'"
# Get product prices
--eval="JSON.stringify(Array.from(document.querySelectorAll('.price')).map(p => p.innerText))"# Base command pattern (always use these flags)
INTERACT="${CLAUDE_PLUGIN_ROOT}/bin/interact --no-headless --user-data=~/.chrome-instacart"
NAVIGATE="${CLAUDE_PLUGIN_ROOT}/bin/navigate --no-headless --user-data=~/.chrome-instacart"
# Suppress stderr noise
$INTERACT --eval="..." 2>/dev/null# Go to homepage first
$NAVIGATE "https://www.instacart.com"
# Find store links on page
$INTERACT --eval="Array.from(document.querySelectorAll('a')).filter(a => a.innerText.includes('Stop & Shop')).map(a => ({text: a.innerText, href: a.href}))"
# Click the store link found above (or navigate to storefront)
$NAVIGATE "https://www.instacart.com/store/stop-shop/storefront"# Step 1: Clear existing search (IMPORTANT - prevents concatenation)
$INTERACT --eval="var input = document.querySelector('#search-bar-input'); input.select(); document.execCommand('delete'); 'cleared'" 2>/dev/null
# Step 2: Type search term
$INTERACT --type="#search-bar-input=pork shoulder" 2>/dev/null
# Step 3: Submit form (dispatchEvent is most reliable for SPAs)
$INTERACT --eval="document.querySelector('#search-bar-input').closest('form').dispatchEvent(new Event('submit', {bubbles: true, cancelable: true})); 'submitted'" 2>/dev/null && sleep 3$INTERACT --eval="var input = document.querySelector('#search-bar-input'); input.select(); document.execCommand('delete'); 'cleared'" 2>/dev/null && \
$INTERACT --type="#search-bar-input=SEARCH_TERM" 2>/dev/null && \
$INTERACT --eval="document.querySelector('#search-bar-input').closest('form').dispatchEvent(new Event('submit', {bubbles: true, cancelable: true})); 'submitted'" 2>/dev/null && sleep 3$INTERACT --eval="JSON.stringify(Array.from(document.querySelectorAll('button[aria-label*=\"Add\"]')).slice(0,8).map(b => b.getAttribute('aria-label')))" 2>/dev/null$INTERACT --eval="document.body.innerText.substring(0, 4000)" 2>/dev/null$INTERACT --eval="var btn = document.querySelector('button[aria-label=\"Add 1 ct Store Brand Baby Spinach\"]'); if(btn) { btn.click(); 'Added'; } else { 'Not found'; }" 2>/dev/null$INTERACT --eval="var btn = document.querySelector('button[aria-label*=\"Add\"][aria-label*=\"Spinach\"]'); if(btn) { btn.click(); 'Added'; } else { 'Not found'; }" 2>/dev/null$INTERACT --eval="var btn = document.querySelector('button[aria-label*=\"Increment\"][aria-label*=\"Avocado\"]'); if(btn) { btn.click(); 'Incremented'; } else { 'Not found'; }" 2>/dev/null# Add first one, then increment 5 times
$INTERACT --eval="document.querySelector('button[aria-label*=\"Add\"][aria-label*=\"Avocado\"]').click(); 'Added 1'" 2>/dev/null && sleep 1
for i in 1 2 3 4 5; do
sleep 0.5
$INTERACT --eval="var btn = document.querySelector('button[aria-label*=\"Increment\"][aria-label*=\"Avocado\"]'); if(btn) btn.click(); 'Inc'" 2>/dev/null
done$INTERACT --eval="for(let i=0;i<5;i++){var btn=document.querySelector('button[aria-label*=\"Increment\"][aria-label*=\"Lemon\"]');if(btn)btn.click();} 'Incremented to 6'" 2>/dev/null$INTERACT --eval="var btn = document.querySelector('button[aria-label=\"Remove Bertolli Original Extra Virgin Olive Oil\"]'); if(btn) { btn.click(); 'Removed'; } else { 'Not found'; }" 2>/dev/null$INTERACT --eval="var badge = Array.from(document.querySelectorAll('*')).find(el => /^\\d+$/.test(el.innerText.trim()) && parseInt(el.innerText) > 10 && el.offsetWidth < 50); if(badge) { var parent = badge.closest('a') || badge.closest('button') || badge.parentElement.parentElement; if(parent) parent.click(); 'clicked cart'; } else { 'cart not found'; }" 2>/dev/null && sleep 2$INTERACT --eval="document.body.innerText.match(/\\$\\d{2,3}\\.\\d{2}/) ? document.body.innerText.match(/\\$\\d{2,3}\\.\\d{2}/)[0] : 'no total found'" 2>/dev/null$INTERACT --eval="document.body.innerText.substring(5500, 12000)" 2>/dev/null$INTERACT --eval="Array.from(document.querySelectorAll('button')).filter(b => b.getAttribute('aria-label') && (b.getAttribute('aria-label').includes('Remove') || b.getAttribute('aria-label').includes('Decrement'))).slice(0,20).map(b => b.getAttribute('aria-label')).join(' | ')" 2>/dev/null# Function to search and add item
add_item() {
local search="$1"
local product="$2"
local qty="${3:-1}"
# Search
$INTERACT --eval="var input = document.querySelector('#search-bar-input'); input.select(); document.execCommand('delete'); 'cleared'" 2>/dev/null
$INTERACT --type="#search-bar-input=$search" 2>/dev/null
$INTERACT --eval="document.querySelector('#search-bar-input').closest('form').dispatchEvent(new Event('submit', {bubbles: true, cancelable: true})); 'submitted'" 2>/dev/null
sleep 3
# Add
$INTERACT --eval="var btn = document.querySelector('button[aria-label*=\"Add\"][aria-label*=\"$product\"]'); if(btn) { btn.click(); 'Added'; } else { 'Not found'; }" 2>/dev/null
sleep 1
# Increment if qty > 1
if [ "$qty" -gt 1 ]; then
for i in $(seq 2 $qty); do
sleep 0.5
$INTERACT --eval="var btn = document.querySelector('button[aria-label*=\"Increment\"][aria-label*=\"$product\"]'); if(btn) btn.click();" 2>/dev/null
done
fi
}
# Usage examples:
add_item "avocados" "Hass Avocado" 6
add_item "baby spinach" "Store Brand Baby Spinach" 2
add_item "bell peppers" "Green Bell Pepper" 2