CLI tool for interacting with the web
A Go CLI tool that drives a persistent headless Chrome instance using the rod browser automation library. Each command connects to the same long-running Chrome process, making it easy to script multi-step browser interactions from shell scripts or interactive use.
rodney start → launches Chrome (headless, persists after CLI exits)
saves WebSocket debug URL to ~/.rodney/state.json
rodney connect H:P → connects to an existing Chrome on a remote debug port
saves WebSocket debug URL to ~/.rodney/state.json
rodney open URL → connects to running Chrome via WebSocket
navigates the active tab, disconnects
rodney js EXPR → connects, evaluates JS, prints result, disconnects
rodney stop → connects and shuts down Chrome, cleans up stateEach CLI invocation is a short-lived process. Chrome runs independently and tabs persist between commands.
go build -o rodney .Requires:
ROD_CHROME_BIN=/path/to/chrome)rodney start # Launch headless Chrome
rodney start --show # Launch with visible browser window
rodney start --insecure # Launch with TLS errors ignored (-k shorthand)
rodney connect host:9222 # Connect to existing Chrome on remote debug port
rodney status # Show browser info and active page
rodney stop # Shut down Chromerodney open https://example.com # Navigate to URL
rodney open example.com # http:// prefix added automatically
rodney back # Go back
rodney forward # Go forward
rodney reload # Reload page
rodney reload --hard # Reload bypassing cache
rodney clear-cache # Clear the browser cacherodney url # Print current URL
rodney title # Print page title
rodney text "h1" # Print text content of element
rodney html "div.content" # Print outer HTML of element
rodney html # Print full page HTML
rodney attr "a#link" href # Print attribute value
rodney pdf output.pdf # Save page as PDFrodney js document.title # Evaluate expression
rodney js "1 + 2" # Math
rodney js 'document.querySelector("h1").textContent' # DOM queries
rodney js '[1,2,3].map(x => x * 2)' # Returns pretty-printed JSON
rodney js 'document.querySelectorAll("a").length' # Count elementsThe expression is automatically wrapped in () => { return (expr); }.
…rodney wait ".loaded" # Wait for element to appear and be visible
rodney waitload # Wait for page load event
rodney waitstable # Wait for DOM to stop changing
rodney waitidle # Wait for network to be idle
rodney sleep 2.5 # Sleep for N secondsrodney screenshot # Save as screenshot.png
rodney screenshot page.png # Save to specific file
rodney screenshot -w 1280 -h 720 out.png # Set viewport width/height
rodney screenshot-el ".chart" chart.png # Screenshot specific elementrodney pages # List all tabs (* marks active)
rodney newpage https://... # Open URL in new tab
rodney page 1 # Switch to tab by index
rodney closepage 1 # Close tab by index
rodney closepage # Close active tabrodney exists ".loading" # Exit 0 if exists, exit 1 if not
rodney count "li.item" # Print number of matching elements
rodney visible "#modal" # Exit 0 if visible, exit 1 if not
rodney assert 'document.title' 'Home' # Exit 0 if equal, exit 1 if not
rodney assert 'document.querySelector("h1") !== null' # Exit 0 if truthyrodney ax-tree # Dump full accessibility tree
rodney ax-tree --depth 3 # Limit tree depth
rodney ax-tree --json # Output as JSON
rodney ax-find --role button # Find all buttons
rodney ax-find --name "Submit" # Find by accessible name
rodney ax-find --role link --name "Home" # Combine filters
rodney ax-find --role button --json # Output as JSON
rodney ax-node "#submit-btn" # Inspect element's a11y properties
rodney ax-node "h1" --json # Output as JSONThese commands use Chrome's Accessibility CDP domain to expose what assistive technologies see. ax-tree uses getFullAXTree, ax-find uses queryAXTree, and ax-node uses getPartialAXTree.
# CI check: verify all buttons have accessible names
rodney ax-find --role button --json | python3 -c "
import json, sys
buttons = json.load(sys.stdin)
unnamed = [b for b in buttons if not b.get('name', {}).get('value')]
if unnamed:
print(f'FAIL: {len(unnamed)} button(s) missing accessible name')
sys.exit(1)
print(f'PASS: all {len(buttons)} buttons have accessible names')
"By default, Rodney stores state globally in ~/.rodney/. You can instead create a session scoped to the current directory with --local:
rodney start --local # State stored in ./.rodney/state.json
# Chrome data in ./.rodney/chrome-data/
rodney open https://example.com # Auto-detects local session
rodney stop # Cleans up local sessionThis is useful when you want isolated browser sessions per project — each directory gets its own Chrome instance, cookies, and state.
Auto-detection: When neither --local nor --global is specified, Rodney checks for ./.rodney/state.json in the current directory. If found, it uses the local session; otherwise it falls back to the global ~/.rodney/ session.
# Force global even when a local session exists
rodney --global open https://example.com
# Force local (errors if no local session)
rodney --local statusThe --local and --global flags can appear anywhere in the command:
rodney --local start
rodney start --local # Same effect
rodney open --global https://example.comAdd .rodney/ to your .gitignore to keep session state out of version control.
# Wait for page to load and extract data
rodney start
rodney open https://example.com
rodney waitstable
title=$(rodney title)
echo "Page: $title"
# Conditional logic based on element presence
if rodney exists ".error-message"; then
rodney text ".error-message"
fi
# Loop through pages
for url in page1 page2 page3; do
rodney open "https://example.com/$url"
rodney waitstable
rodney screenshot "${url}.png"
done
rodney stopRodney uses distinct exit codes to separate check failures from errors:
| Exit code | Meaning |
|---|---|
0 |
Success |
1 |
Check failed — the command ran successfully but the condition/assertion was not met |
2 |
Error — something went wrong (bad arguments, no browser session, timeout, etc.) |
This makes it easy to distinguish between "the assertion is false" and "the command couldn't run" in scripts and CI pipelines.
Several commands return exit code 1 when a condition is not met, making them useful as assertions in shell scripts and CI pipelines. All of these print their result to stdout and exit cleanly — no error message is written to stderr.
exists — check if an element exists in the DOMrodney exists "h1"
# Prints "true", exits 0
rodney exists ".nonexistent"
# Prints "false", exits 1visible — check if an element is visiblerodney visible "#modal"
# Prints "true" and exits 0 if the element exists and is visible
rodney visible "#hidden-div"
# Prints "false" and exits 1 if the element is hidden or doesn't existax-find — check for accessibility nodesrodney ax-find --role button --name "Submit"
# Prints the matching node(s), exits 0
rodney ax-find --role banner --name "Nonexistent"
# Prints "No matching nodes" to stderr, exits 1assert — assert a JavaScript expressionWith one argument, checks that the expression is truthy. With two arguments, checks that the expression's value equals the expected string. Use --message / -m to set a custom failure message.
…The expression is evaluated the same way as rodney js — the result is converted to its string representation before comparison. This means rodney assert 'document.title' 'Dashboard' compares the unquoted string, and rodney assert '1 + 2' '3' compares the number as a string.
Use --message (or -m) to add a human-readable description to the failure output:
rodney assert 'document.querySelector(".logged-in")' -m "User should be logged in"
# On failure: "fail: User should be logged in (got null)"
rodney assert 'document.title' 'Dashboard' --message "Wrong page loaded"
# On failure: 'fail: Wrong page loaded (got "Home", expected "Dashboard")'You can chain these together in a single script to run multiple assertions. Because check failures use exit code 1 while real errors use exit code 2, you can use set -e to abort on errors while handling check failures explicitly:
…This pattern is useful in CI — run Rodney as a post-deploy check, an accessibility audit, or a smoke test against a staging environment. Because exit code 2 signals an actual error (e.g. Chrome didn't start), set -e will abort the script immediately if something is broken rather than reporting a misleading test failure.
| Environment Variable | Default | Description |
|---|---|---|
RODNEY_HOME |
~/.rodney |
Data directory for state and Chrome profile |
ROD_CHROME_BIN |
/usr/bin/google-chrome |
Path to Chrome/Chromium binary |
ROD_TIMEOUT |
30 |
Default timeout in seconds for element queries |
HTTPS_PROXY / HTTP_PROXY |
(none) | Authenticated proxy auto-detected on start |
Global state is stored in ~/.rodney/state.json with Chrome user data in ~/.rodney/chrome-data/. When using --local, state is stored in ./.rodney/state.json and ./.rodney/chrome-data/ in the current directory instead. Set RODNEY_HOME to override the default global directory.
In environments with authenticated HTTP proxies (e.g., HTTPS_PROXY=http://user:pass@host:port), rodney start automatically:
Proxy-Authorization headers into CONNECT requestsThis is necessary because Chrome cannot natively authenticate to proxies during HTTPS tunnel (CONNECT) establishment. The local proxy runs as a background process and is automatically cleaned up by rodney stop.
See claude-code-chrome-proxy.md for detailed technical notes.
The tool uses the rod Go library which communicates with Chrome via the DevTools Protocol (CDP) over WebSocket. Key implementation details:
start uses rod's launcher package to start Chrome with Leakless(false) so Chrome survives after the CLI exitsNo open issues yet, or sync has not completed.