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 state Each CLI invocation is a short-lived process. Chrome runs independently and tabs persist between commands.
go build -o rodney .Requires:
- Go 1.21+
- Google Chrome or Chromium installed (or set
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 click "button#submit" # Click element rodney input "#search" "query" # Type into input field rodney clear "#search" # Clear input field rodney file "#upload" photo.png # Set file on a file input rodney file "#upload" - # Set file from stdin rodney download "a.pdf-link" # Download href/src target to file rodney download "a.pdf-link" - # Download to stdout rodney select "#dropdown" "value" # Select dropdown by value rodney submit "form#login" # Submit a form rodney hover ".menu-item" # Hover over element rodney focus "#email" # Focus elementrodney 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.
rodney exists "h1" # Prints "true", exits 0 rodney exists ".nonexistent" # Prints "false", exits 1rodney 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 existrodney 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 1With 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.
# Truthy mode — check that expression evaluates to a truthy value rodney assert 'document.querySelector(".logged-in") !== null' # Prints "pass", exits 0 rodney assert 'document.querySelector(".nonexistent")' # Prints "fail: got null", exits 1 # Equality mode — check that expression result matches expected value rodney assert 'document.title' 'Dashboard' # Prints "pass" if title is "Dashboard", exits 0 rodney assert 'document.querySelectorAll(".item").length' '3' # Prints "pass" if there are exactly 3 items, exits 0 rodney assert 'document.title' 'Wrong Title' # Prints 'fail: got "Dashboard", expected "Wrong Title"', exits 1The 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:
#!/bin/bash set -euo pipefail FAIL=0 check() { if ! "$@"; then echo "FAIL: $*" FAIL=1 fi } rodney start rodney open "https://example.com" rodney waitstable # Assert elements exist check rodney exists "h1" check rodney exists "nav" check rodney exists "footer" # Assert key elements are visible check rodney visible "h1" check rodney visible "#main-content" # Assert JS expressions check rodney assert 'document.title' 'Example Domain' check rodney assert 'document.querySelectorAll("p").length' '2' check rodney assert 'document.querySelector("h1") !== null' # Assert accessibility requirements check rodney ax-find --role navigation check rodney ax-find --role heading --name "Example Domain" rodney stop if [ "$FAIL" -ne 0 ]; then echo "Some checks failed" exit 1 fi echo "All checks passed"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:
- Detects the proxy credentials from environment variables
- Launches a local forwarding proxy that injects
Proxy-Authorizationheaders into CONNECT requests - Configures Chrome to use the local proxy
This 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:
startuses rod'slauncherpackage to start Chrome withLeakless(false)so Chrome survives after the CLI exits- Proxy auth handled via a local forwarding proxy that bridges Chrome to authenticated upstream proxies
- State persistence via a JSON file containing the WebSocket debug URL and Chrome PID
- Each command creates a new rod
Browserconnection to the same Chrome instance, executes the operation, and disconnects - Element queries use rod's built-in auto-wait with a configurable timeout (default 30s)
- JS evaluation wraps user expressions in arrow functions as required by rod's
Eval - Accessibility commands call CDP's Accessibility domain directly via rod's
protopackage (getFullAXTree,queryAXTree,getPartialAXTree)
- github.com/go-rod/rod v0.116.2 - Chrome DevTools Protocol automation
| Command | Arguments | Description |
|---|---|---|
start | [--show] [--insecure|-k] | Launch Chrome (headless by default, --show for visible) |
connect | <host:port> | Connect to existing Chrome on remote debug port |
stop | Shut down Chrome | |
status | Show browser status | |
open | <url> | Navigate to URL |
back | Go back in history | |
forward | Go forward in history | |
reload | [--hard] | Reload page (--hard bypasses cache) |
clear-cache | Clear the browser cache | |
url | Print current URL | |
title | Print page title | |
html | [selector] | Print HTML (page or element) |
text | <selector> | Print element text content |
attr | <selector> <name> | Print attribute value |
pdf | [file] | Save page as PDF |
js | <expression> | Evaluate JavaScript |
click | <selector> | Click element |
input | <selector> <text> | Type into input |
clear | <selector> | Clear input |
file | <selector> <path|-> | Set file on a file input (- for stdin) |
download | <selector> [file|-] | Download href/src target (- for stdout) |
select | <selector> <value> | Select dropdown value |
submit | <selector> | Submit form |
hover | <selector> | Hover over element |
focus | <selector> | Focus element |
wait | <selector> | Wait for element to appear |
waitload | Wait for page load | |
waitstable | Wait for DOM stability | |
waitidle | Wait for network idle | |
sleep | <seconds> | Sleep N seconds |
screenshot | [-w N] [-h N] [file] | Page screenshot (optional viewport size) |
screenshot-el | <selector> [file] | Element screenshot |
pages | List tabs | |
page | <index> | Switch tab |
newpage | [url] | Open new tab |
closepage | [index] | Close tab |
exists | <selector> | Check element exists (exit 1 if not) |
count | <selector> | Count matching elements |
visible | <selector> | Check element visible (exit 1 if not) |
assert | <expr> [expected] [-m msg] | Assert JS expression is truthy or equals expected (exit 1 if not) |
ax-tree | [--depth N] [--json] | Dump accessibility tree |
ax-find | [--name N] [--role R] [--json] | Find accessible nodes |
ax-node | <selector> [--json] | Show element accessibility info |
| Flag | Description |
|---|---|
--local | Use directory-scoped session (./.rodney/) |
--global | Use global session (~/.rodney/) |
--version | Print version and exit |
--help, -h, help | Show help message |