Comprehensive guide for browser automation and web scraping with go-rod (Chrome DevTools Protocol) including stealth anti-bot-detection patterns.
Add this skill
npx mdskills install sickn33/go-rod-masterComprehensive browser automation guide with excellent stealth patterns, code examples, and edge cases
Rod is a high-level Go driver built directly on the Chrome DevTools Protocol for browser automation and web scraping. Unlike wrappers around other tools, Rod communicates with the browser natively via CDP, providing thread-safe operations, chained context design for timeouts/cancellation, auto-wait for elements, correct iframe/shadow DOM handling, and zero zombie browser processes.
The companion library go-rod/stealth injects anti-bot-detection evasions based on puppeteer-extra stealth, hiding headless browser fingerprints from detection systems.
Risk Level: ๐ต Safe
defer pattern โ browsers and pages close automatically.# Core rod library
go get github.com/go-rod/rod@latest
# Stealth anti-detection plugin (ALWAYS include for production scraping)
go get github.com/go-rod/stealth@latest
Rod auto-downloads a compatible Chromium binary on first run. To pre-download:
go run github.com/nichochar/go-rod.github.io/cmd/launcher@latest
Rod manages three layers: Browser โ Page โ Element.
// Launch and connect to a browser
browser := rod.New().MustConnect()
defer browser.MustClose()
// Create a page (tab)
page := browser.MustPage("https://example.com")
// Find an element
el := page.MustElement("h1")
fmt.Println(el.MustText())
Rod provides two API styles for every operation:
| Style | Method | Use Case |
|---|---|---|
| Must | MustElement(), MustClick(), MustText() | Scripting, debugging, prototyping. Panics on error. |
| Error | Element(), Click(), Text() | Production code. Returns error for explicit handling. |
Production pattern:
el, err := page.Element("#login-btn")
if err != nil {
return fmt.Errorf("login button not found: %w", err)
}
if err := el.Click(proto.InputMouseButtonLeft, 1); err != nil {
return fmt.Errorf("click failed: %w", err)
}
Scripting pattern with Try:
err := rod.Try(func() {
page.MustElement("#login-btn").MustClick()
})
if errors.Is(err, context.DeadlineExceeded) {
log.Println("timeout finding login button")
}
Rod uses Go's context.Context for cancellation and timeouts. Context propagates recursively to all child operations.
// Set a 5-second timeout for the entire operation chain
page.Timeout(5 * time.Second).
MustWaitLoad().
MustElement("title").
CancelTimeout(). // subsequent calls are not bound by the 5s timeout
Timeout(30 * time.Second).
MustText()
Rod supports multiple selector strategies:
// CSS selector (most common)
page.MustElement("div.content > p.intro")
// CSS selector with text regex matching
page.MustElementR("button", "Submit|Send")
// XPath
page.MustElementX("//div[@class='content']//p")
// Search across iframes and shadow DOM (like DevTools Ctrl+F)
page.MustSearch(".deeply-nested-element")
Rod automatically retries element queries until the element appears or the context times out. You do not need manual sleeps:
// This will automatically wait until the element exists
el := page.MustElement("#dynamic-content")
// Wait until the element is stable (position/size not changing)
el.MustWaitStable().MustClick()
// Wait until page has no pending network requests
wait := page.MustWaitRequestIdle()
page.MustElement("#search").MustInput("query")
wait()
IMPORTANT: For any production scraping or automation against real websites, ALWAYS use
stealth.MustPage()instead ofbrowser.MustPage(). This is the single most important step for avoiding bot detection.
The go-rod/stealth package injects JavaScript evasions into every new page that:
navigator.webdriver โ the primary headless detection signal.PluginArray type with realistic plugin count."prompt" instead of bot-revealing values.en-US,en instead of empty arrays.Creating a stealth page (recommended for all production use):
import (
"github.com/go-rod/rod"
"github.com/go-rod/stealth"
)
browser := rod.New().MustConnect()
defer browser.MustClose()
// Use stealth.MustPage instead of browser.MustPage
page := stealth.MustPage(browser)
page.MustNavigate("https://bot.sannysoft.com")
With error handling:
page, err := stealth.Page(browser)
if err != nil {
return fmt.Errorf("failed to create stealth page: %w", err)
}
page.MustNavigate("https://example.com")
Using stealth.JS directly (advanced โ for custom page creation):
// If you need to create the page yourself (e.g., with specific options),
// inject stealth.JS manually via EvalOnNewDocument
page := browser.MustPage()
page.MustEvalOnNewDocument(stealth.JS)
page.MustNavigate("https://example.com")
Navigate to a bot detection test page to verify evasions:
page := stealth.MustPage(browser)
page.MustNavigate("https://bot.sannysoft.com")
page.MustScreenshot("stealth_test.png")
Expected results for a properly stealth-configured browser:
missing (passed)present (passed)3 (not 0)en-US,enUse the launcher package to customize browser launch flags:
import "github.com/go-rod/rod/lib/launcher"
url := launcher.New().
Headless(true). // false for debugging
Proxy("127.0.0.1:8080"). // upstream proxy
Set("disable-gpu", ""). // custom Chrome flag
Delete("use-mock-keychain"). // remove a default flag
MustLaunch()
browser := rod.New().ControlURL(url).MustConnect()
defer browser.MustClose()
Debugging mode (visible browser + slow motion):
l := launcher.New().
Headless(false).
Devtools(true)
defer l.Cleanup()
browser := rod.New().
ControlURL(l.MustLaunch()).
Trace(true).
SlowMotion(2 * time.Second).
MustConnect()
// Set proxy at launch
url := launcher.New().
Proxy("socks5://127.0.0.1:1080").
MustLaunch()
browser := rod.New().ControlURL(url).MustConnect()
// Handle proxy authentication
go browser.MustHandleAuth("username", "password")()
// Ignore SSL certificate errors (for MITM proxies)
browser.MustIgnoreCertErrors(true)
import "github.com/go-rod/rod/lib/input"
// Type into an input field (replaces existing value)
page.MustElement("#email").MustInput("user@example.com")
// Simulate keyboard keys
page.Keyboard.MustType(input.Enter)
// Press key combinations
page.Keyboard.MustPress(input.ControlLeft)
page.Keyboard.MustType(input.KeyA)
page.Keyboard.MustRelease(input.ControlLeft)
// Mouse click at coordinates
page.Mouse.MustClick(input.MouseLeft)
page.Mouse.MustMoveTo(100, 200)
router := browser.HijackRequests()
defer router.MustStop()
// Block all image requests
router.MustAdd("*.png", func(ctx *rod.Hijack) {
ctx.Response.Fail(proto.NetworkErrorReasonBlockedByClient)
})
// Modify request headers
router.MustAdd("*api.example.com*", func(ctx *rod.Hijack) {
ctx.Request.Req().Header.Set("Authorization", "Bearer token123")
ctx.MustLoadResponse()
})
// Modify response body
router.MustAdd("*.js", func(ctx *rod.Hijack) {
ctx.MustLoadResponse()
ctx.Response.SetBody(ctx.Response.Body() + "\n// injected")
})
go router.Run()
// Wait for page load event
page.MustWaitLoad()
// Wait for no pending network requests (AJAX idle)
wait := page.MustWaitRequestIdle()
page.MustElement("#search").MustInput("query")
wait()
// Wait for element to be stable (not animating)
page.MustElement(".modal").MustWaitStable().MustClick()
// Wait for element to become invisible
page.MustElement(".loading").MustWaitInvisible()
// Wait for JavaScript condition
page.MustWait(`() => document.title === 'Ready'`)
// Wait for specific navigation/event
wait := page.WaitEvent(&proto.PageLoadEventFired{})
page.MustNavigate("https://example.com")
wait()
Handle pages where the result can be one of several outcomes (e.g., login success vs error):
page.MustElement("#username").MustInput("user")
page.MustElement("#password").MustInput("pass").MustType(input.Enter)
// Race between success and error selectors
elm := page.Race().
Element(".dashboard").MustHandle(func(e *rod.Element) {
fmt.Println("Login successful:", e.MustText())
}).
Element(".error-message").MustDo()
if elm.MustMatches(".error-message") {
log.Fatal("Login failed:", elm.MustText())
}
// Full-page screenshot
page.MustScreenshot("page.png")
// Custom screenshot (JPEG, specific region)
img, _ := page.Screenshot(true, &proto.PageCaptureScreenshot{
Format: proto.PageCaptureScreenshotFormatJpeg,
Quality: gson.Int(90),
Clip: &proto.PageViewport{
X: 0, Y: 0, Width: 1280, Height: 800, Scale: 1,
},
})
utils.OutputFile("screenshot.jpg", img)
// Scroll screenshot (captures full scrollable page)
img, _ := page.MustWaitStable().ScrollScreenshot(nil)
utils.OutputFile("full_page.jpg", img)
// PDF export
page.MustPDF("output.pdf")
pool := rod.NewPagePool(5) // max 5 concurrent pages
create := func() *rod.Page {
return browser.MustIncognito().MustPage()
}
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
page := pool.MustGet(create)
defer pool.Put(page)
page.MustNavigate(u).MustWaitLoad()
fmt.Println(page.MustInfo().Title)
}(url)
}
wg.Wait()
pool.Cleanup(func(p *rod.Page) { p.MustClose() })
// Listen for console.log output
go page.EachEvent(func(e *proto.RuntimeConsoleAPICalled) {
if e.Type == proto.RuntimeConsoleAPICalledTypeLog {
fmt.Println(page.MustObjectsToJSON(e.Args))
}
})()
// Wait for a specific event before proceeding
wait := page.WaitEvent(&proto.PageLoadEventFired{})
page.MustNavigate("https://example.com")
wait()
wait := browser.MustWaitDownload()
page.MustElementR("a", "Download PDF").MustClick()
data := wait()
utils.OutputFile("downloaded.pdf", data)
// Execute JS on the page
page.MustEval(`() => console.log("hello")`)
// Pass parameters and get return value
result := page.MustEval(`(a, b) => a + b`, 1, 2)
fmt.Println(result.Int()) // 3
// Eval on a specific element ("this" = the DOM element)
title := page.MustElement("title").MustEval(`() => this.innerText`).String()
// Direct CDP calls for features Rod doesn't wrap
proto.PageSetAdBlockingEnabled{Enabled: true}.Call(page)
extPath, _ := filepath.Abs("./my-extension")
u := launcher.New().
Set("load-extension", extPath).
Headless(false). // extensions require headed mode
MustLaunch()
browser := rod.New().ControlURL(u).MustConnect()
See the examples/ directory for complete, runnable Go files:
examples/basic_scrape.go โ Minimal scraping exampleexamples/stealth_page.go โ Anti-detection with go-rod/stealthexamples/request_hijacking.go โ Intercepting and modifying network requestsexamples/concurrent_pages.go โ Page pool for concurrent scrapingstealth.MustPage(browser) instead of browser.MustPage() for real-world sites.defer browser.MustClose() immediately after connecting.Must*) in production code..Timeout() โ never rely on defaults for production.browser.MustIncognito().MustPage() for isolated sessions.PagePool for concurrent scraping instead of spawning unlimited pages.MustWaitStable() before clicking elements that might be animating.MustWaitRequestIdle() after actions that trigger AJAX calls.launcher.New().Headless(false).Devtools(true) for debugging.time.Sleep() for waiting โ use Rod's built-in wait methods.Browser per task โ create one Browser, use multiple Page instances.browser.MustPage() for production scraping โ use stealth.MustPage().Problem: Element not found even though it exists on the page.
Solution: The element may be inside an iframe or shadow DOM. Use page.MustSearch() instead of page.MustElement() โ it searches across all iframes and shadow DOMs.
Problem: Click doesn't work because the element is animating.
Solution: Call el.MustWaitStable() before el.MustClick().
Problem: Bot detection despite using stealth.
Solution: Combine stealth.MustPage() with: randomized viewport sizes, realistic User-Agent strings, human-like input delays between keystrokes, and random idle behaviors (scroll, hover).
Problem: Browser process leaks (zombie processes).
Solution: Always defer browser.MustClose(). Rod uses leakless to kill zombies after main process crash, but explicit cleanup is preferred.
Problem: Timeout errors on slow pages.
Solution: Use chained context: page.Timeout(30 * time.Second).MustWaitLoad(). For AJAX-heavy pages, use MustWaitRequestIdle() instead of MustWaitLoad().
Problem: HijackRequests router not intercepting requests.
Solution: You must call go router.Run() after setting up routes, and defer router.MustStop() for cleanup.
go-rod/stealth handles common detection (WebDriver, plugin fingerprints, WebGL), extremely strict systems (some Cloudflare configurations, Akamai Bot Manager) may still detect automation. Additional measures (residential proxies, human-like behavioral patterns) may be needed.PagePool and limit concurrency on memory-constrained systems.Headless(false) with XVFB for server environments.references/api-reference.md โ Quick-reference cheat sheetInstall via CLI
npx mdskills install sickn33/go-rod-masterGo Rod Master is a free, open-source AI agent skill. Comprehensive guide for browser automation and web scraping with go-rod (Chrome DevTools Protocol) including stealth anti-bot-detection patterns.
Install Go Rod Master with a single command:
npx mdskills install sickn33/go-rod-masterThis downloads the skill files into your project and your AI agent picks them up automatically.
Go Rod Master works with Claude Code, Claude Desktop, Cursor, Vscode Copilot, Windsurf, Continue Dev, Codex, Gemini Cli, Amp, Roo Code, Goose, Opencode, Trae, Qodo, Command Code. Skills use the open SKILL.md format which is compatible with any AI coding agent that reads markdown instructions.