Agent Skills: Browser Automation with Chrome DevTools Protocol

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.

UncategorizedID: The-Focus-AI/chrome-driver/browser-automation

Install this agent skill to your local

pnpm dlx add-skill https://github.com/The-Focus-AI/chrome-driver/tree/HEAD/skills/browser-automation

Skill Files

Browse the full folder contents for browser-automation.

Download Skill

Loading file tree…

skills/browser-automation/SKILL.md

Skill Metadata

Name
browser-automation
Description
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.

Browser Automation with Chrome DevTools Protocol

Control Chrome browser programmatically using simple command-line scripts. All scripts auto-start Chrome in headless mode if not running.

Global Options (All Commands)

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 runs

Example: Persistent session for logged-in sites

# First time: login interactively with visible browser
${CLAUDE_PLUGIN_ROOT}/bin/navigate https://instacart.com --no-headless --user-data=~/.chrome-instacart

# After logging in manually, future commands use same profile (cookies preserved)
${CLAUDE_PLUGIN_ROOT}/bin/navigate https://instacart.com/store/market-32 --user-data=~/.chrome-instacart
${CLAUDE_PLUGIN_ROOT}/bin/screenshot https://instacart.com/account --user-data=~/.chrome-instacart /tmp/account.png

Available Commands

All scripts are in ${CLAUDE_PLUGIN_ROOT}/bin/:

screenshot - Capture web pages as images

${CLAUDE_PLUGIN_ROOT}/bin/screenshot URL [OUTPUT] [OPTIONS]

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:

# 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"

pdf - Generate PDFs from web pages

${CLAUDE_PLUGIN_ROOT}/bin/pdf URL [OUTPUT] [OPTIONS]

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/images

Examples:

# 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

extract - Extract content from web pages

${CLAUDE_PLUGIN_ROOT}/bin/extract URL [OPTIONS]

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 metadata

Examples:

# 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

navigate - Navigate and interact with pages

${CLAUDE_PLUGIN_ROOT}/bin/navigate URL [OPTIONS]

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:

# 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"

form - Fill out and submit web forms

${CLAUDE_PLUGIN_ROOT}/bin/form URL [OPTIONS]

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 completion

Examples:

# 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'

record - Record screencast frames

${CLAUDE_PLUGIN_ROOT}/bin/record URL OUTPUT_DIR [OPTIONS]

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:

# 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

interact - Interact with current page (no navigation)

${CLAUDE_PLUGIN_ROOT}/bin/interact [OPTIONS]

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:

  • Multi-step processes (search → click result → add to cart)
  • Single-page applications (SPAs)
  • Sites where navigation resets state

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:

# 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

chrome-status - Check browser status

${CLAUDE_PLUGIN_ROOT}/bin/chrome-status

Shows whether Chrome is running, version info, and open tabs.

Common Workflows

Screenshot a page

${CLAUDE_PLUGIN_ROOT}/bin/screenshot https://example.com /tmp/screenshot.png

Convert page to PDF

${CLAUDE_PLUGIN_ROOT}/bin/pdf https://example.com /tmp/document.pdf --paper=a4

Scrape page content

${CLAUDE_PLUGIN_ROOT}/bin/extract https://example.com --format=markdown

Fill and submit a form

${CLAUDE_PLUGIN_ROOT}/bin/form https://example.com/login \
  --fill='#username=user' \
  --fill='#password=pass' \
  --submit='button[type=submit]' \
  --wait-after='.dashboard'

Record a screencast

${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

Notes

  • Chrome auto-starts in headless mode when needed (use --no-headless for visible browser)
  • Chrome continues running between commands for speed
  • Use --user-data=PATH to preserve login/cookies between sessions
  • Use pkill -f 'chrome.*--remote-debugging-port' to stop Chrome manually
  • Default port is 9222; set CHROME_DRIVER_PORT to change
  • All scripts support --help for full usage info
  • Large screenshots are auto-scaled to fit within 8000px (API limit)

Session Management Tips

For sites requiring login (Instacart, Amazon, banking, etc.):

  1. Create a persistent profile: Use --user-data=~/.chrome-sitename consistently
  2. First-time login: Use --no-headless to see the browser and log in manually
  3. Subsequent automation: Same --user-data path preserves your session
  4. Multiple accounts: Use different --user-data paths for different accounts/sites

Multi-Step Workflow Guide: Shopping Sites (Instacart, Amazon, etc.)

This 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)

Understanding the Two-Command Pattern

Why navigate alone isn't enough:

  • navigate ALWAYS loads/reloads the specified URL
  • Each navigate call is independent - it doesn't "continue" from the previous state
  • Multi-step workflows (search → select → add to cart) break because each step resets

Why interact is essential:

  • interact connects to the EXISTING browser tab
  • It performs actions on whatever page is currently displayed
  • The page state is preserved between interact calls
  • Perfect for SPAs and multi-step processes

Complete Example: Adding Items to Instacart Cart

Here's a real-world example that works:

Step 1: Initial Setup - Create Profile and Login

First time only - open browser visibly so user can log in:

# 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

Step 2: Navigate to Store

After login, go to the desired store:

# 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

Step 3: Search for Product (Multi-Step with interact)

Now use interact to search WITHOUT navigating away:

# 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

Step 4: View Search Results

Check what products are available:

# 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

Step 5: Add Product to Cart

Click the Add button for the desired product:

# 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

Step 6: Verify Cart Updated

Confirm the item was added:

# 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

Key Techniques for Multi-Step Automation

1. Finding Elements with JavaScript

When CSS selectors are complex or dynamic, use --eval to find elements:

# 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})))"

2. Clicking Elements via JavaScript

When --click doesn't work (dynamic elements, overlays, etc.):

# 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'"

3. Submitting Forms

Different approaches for form submission:

# 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'"

4. Waiting for Dynamic Content

Use --wait-for or JavaScript polling:

# 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); })"

5. Reading Page State

Extract information to make decisions:

# 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))"

Troubleshooting Multi-Step Workflows

Problem: Page resets between commands

Solution: Use interact instead of navigate for all steps after initial navigation.

Problem: Elements not found

Solution:

  1. Use --eval to inspect the page structure
  2. Wait for dynamic content with --wait-for or sleep
  3. Check if element is in an iframe

Problem: Click doesn't work

Solution:

  1. Try JavaScript click: --eval="document.querySelector('...').click()"
  2. Scroll element into view first
  3. Check for overlays blocking the element

Problem: Form doesn't submit

Solution:

  1. Use form submit event: form.dispatchEvent(new Event('submit', ...))
  2. Check if it's a SPA with custom form handling
  3. Look for and click the actual submit button

Problem: Login session lost

Solution:

  1. Always use same --user-data path
  2. Check if cookies expired
  3. Some sites require re-login periodically

Best Practices

  1. Always use --user-data for sites requiring login
  2. Use --no-headless during development to see what's happening
  3. Start with navigate to load the initial page
  4. Switch to interact for all subsequent actions on that page
  5. Use --eval liberally to inspect page state and find elements
  6. Add sleep between actions if the site needs time to update
  7. Check results after each step to verify the action worked

Instacart Automation Guide (Tested & Proven Patterns)

This section contains battle-tested patterns for automating Instacart shopping. These patterns were developed through extensive real-world testing.

Quick Reference: Essential Commands

# 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

Store Selection

CRITICAL: Never construct Instacart URLs manually. Always navigate via the UI:

# 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"

Search Pattern (Reliable 3-Step Process)

# 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

Combined one-liner for efficiency:

$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

Product Discovery: Finding Available Items

List available "Add" buttons (shows product names):

$INTERACT --eval="JSON.stringify(Array.from(document.querySelectorAll('button[aria-label*=\"Add\"]')).slice(0,8).map(b => b.getAttribute('aria-label')))" 2>/dev/null

Get page content to see prices and sizes:

$INTERACT --eval="document.body.innerText.substring(0, 4000)" 2>/dev/null

Adding Products to Cart

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):

$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

Add a product (partial match - more flexible):

$INTERACT --eval="var btn = document.querySelector('button[aria-label*=\"Add\"][aria-label*=\"Spinach\"]'); if(btn) { btn.click(); 'Added'; } else { 'Not found'; }" 2>/dev/null

Quantity Management

Increment quantity (add more of same item):

$INTERACT --eval="var btn = document.querySelector('button[aria-label*=\"Increment\"][aria-label*=\"Avocado\"]'); if(btn) { btn.click(); 'Incremented'; } else { 'Not found'; }" 2>/dev/null

Increment multiple times (e.g., get 6 avocados):

# 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

JavaScript loop for multiple increments (faster):

$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

Remove item from cart:

$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

Cart Management

View cart (click cart badge):

$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

Get cart total:

$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

List all items in cart with prices (when cart panel is open):

$INTERACT --eval="document.body.innerText.substring(5500, 12000)" 2>/dev/null

Find all remove/decrement buttons in cart:

$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

Complete Shopping Workflow

Efficient pattern for adding multiple items:

# 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

Troubleshooting Instacart-Specific Issues

Problem: Search terms concatenate (e.g., "lemonsfresh thyme")

  • Cause: Previous search text not cleared
  • Solution: Always clear with input.select(); document.execCommand('delete') before typing

Problem: "Add" button not found

  • Cause: Product name in aria-label doesn't match search
  • Solution:
    1. List available buttons: JSON.stringify(Array.from(document.querySelectorAll('button[aria-label*="Add"]')).slice(0,8).map(b => b.getAttribute('aria-label')))
    2. Use partial match: aria-label*="Add"][aria-label*="partial name"

Problem: Store not available at address

  • Cause: Instacart shows different stores based on delivery address
  • Solution: Check which stores are available at current address, or change address

Problem: Cart shows wrong item count

  • Cause: Some items are sold by weight (e.g., meat shows as 1 item but weighs 5 lbs)
  • Solution: This is normal - check cart details for actual quantities

Problem: Prices differ between stores

  • Cause: Each store has independent pricing
  • Solution: Compare carts between stores, swap expensive items for alternatives

Product Selection Tips

  1. Prefer Store Brand - Usually cheapest option, search results often show store brand
  2. Check sizes - "16 oz" vs "50.7 oz" significantly affects price
  3. Value packs - Look for "Value Pack" in product names for better per-unit pricing
  4. Per-pound items - Meat/produce sold by weight; qty is weight, not count
  5. Watch for sales - "With loyalty card" prices shown in results

Key Lessons Learned

  1. Always use interact after initial navigate - Instacart is an SPA; navigation resets state
  2. Clear search input before new searches - Prevents concatenation bugs
  3. Use aria-label selectors - Most reliable way to find Instacart buttons
  4. Add 2>/dev/null - Suppresses noisy stderr output from Chrome driver
  5. Add sleep 3 after search submit - Allow time for results to load
  6. Partial aria-label matches (*=) - More robust than exact matches
  7. Check cart periodically - Verify items added correctly
  8. Store availability varies by address - Not all stores deliver everywhere