Repository navigation
Commands
Complete command reference for bdg v0.19.0. The authoritative, machine-readable version is bdg --help --json (compact; bdg <command> --help --json describes one command in full); every command also has bdg <command> --help.
bdg <url> # Start session with URL
bdg localhost:3000 # Local development (http:// is added)
bdg https://example.com # Full URLOptions:
| Flag | Description |
|---|---|
-p, --port <number> |
Chrome debugging port (default 9222; the last port is remembered per session directory) |
--stop-after <seconds> |
Stop the session this many seconds after the page has loaded |
--headless / --no-headless
|
Run Chrome without / with a window (default: a window on macOS unless over SSH or CI is set, on Linux when DISPLAY or WAYLAND_DISPLAY is set; CI=false/0 don't count). Agents running unattended on a Mac should pass --headless
|
-u, --user-data-dir <path> |
Chrome profile directory (default: ~/.bdg/chrome-profile) |
--chrome-flags <flags> |
Extra Chrome flags, space-separated (or BDG_CHROME_FLAGS) |
--chrome-ws-url <url> |
Attach to an existing Chrome: its DevTools port (9222, host:port, http://host:port) or a browser/page WebSocket URL. A declined "Allow remote debugging?" prompt exits 82, an unanswered one 102 after 30 s |
--viewport <WxH> |
Exact viewport in CSS px for the whole session, e.g. 1280x800 (default: 1920x1080 window) |
--color-scheme <scheme> |
Emulate prefers-color-scheme: light or dark (default: the system setting) |
--mobile |
Emulate a phone: 390x844 viewport (unless --viewport) at pixel ratio 3 with mobile layout, touch and an Android Chrome user agent |
--session <name> |
Use a named session (own daemon, Chrome and port); env BDG_SESSION. See Named Sessions
|
--capture-all |
Record tracking/analytics domains and capture every response body (incl. binary) |
-m, --max-body-size <megabytes> |
Maximum response body size (default 5) |
-q, --quiet |
Minimal output |
--dialog <accept|dismiss> |
How the session answers JavaScript dialogs, page loads included (default accept; read in any case) |
--state <file> |
Restore cookies and storage saved by bdg state save before the first navigation. With --chrome-ws-url: exit 81 (attach, then bdg state load) |
-j, --json |
JSON envelope with targetUrl, port, chromePid, documentStatus, ... |
The start prints a few lines (-q: one line):
Session Started
Target: https://example.com/
Next: bdg dom layout <selector>, bdg dom query <selector>, bdg dom form, bdg peek, bdg dom screenshot out.png
More: bdg --help (bdg --help --json for agents)
Only one session runs per session name. A second bdg <url> exits 84 and reports the running session. An unreachable URL exits 80. Invalid --viewport / --color-scheme values exit 81.
Ctrl-C at any moment of the start cancels it, also while the page has not answered yet: the navigation is aborted, Chrome is closed (killed if it has not exited 500 ms after Browser.close), and bdg exits 130 (143 for SIGTERM) once the daemon and its Chrome are gone, so the next bdg status or bdg <url> sees no session; a second Ctrl-C exits at once. bdg stop during a slow Chrome start ends it at once. A Chrome that runs but does not open its debugging port within 25 s is reported as such (Chrome started (pid N) but did not open its debugging port …, exit 100), not as a port conflict.
A page that has not finished loading within the start's wait still starts the session (exit 0) with a warning, and JSON adds data.loading: { readyState, pending, pendingCount }:
⚠ The page is still loading (document.readyState: loading); waiting on: GET code.jquery.com/ui/1.13.2/jquery-ui.js (pending 2s). Elements may be missing until it finishes: bdg dom wait <selector> waits for one
# Chrome started with --remote-debugging-port=9222 (e.g. to log in by hand first)
bdg https://app.example.com --chrome-ws-url 9222bdg looks up the WebSocket URL itself. That Chrome keeps running after bdg stop. --port, -u and --[no-]headless cannot be combined with it (exit 81); a Chrome or tab another bdg session uses is refused (exit 90). Downloads are the exception (see Downloads): that Chrome saves them in its own download folder (usually ~/Downloads), and only downloads of the session's page are reported (not those of tabs or popups it opens).
To work on a tab you already have open, without reloading it, use bdg attach:
bdg attach # List the page tabs: [0] title — url
bdg attach checkout # Attach to the tab whose URL or title contains "checkout"
bdg attach 2 # A number is always an index from the list
bdg attach --user-data-dir <dir> # Another Chromium profile (Canary, Edge, Brave)
bdg attach 0 --stop-after 600 # Auto-stop the session after 600 s
bdg attach 0 --dialog accept # Accept JavaScript dialogs (attach default: dismiss)Enable chrome://inspect/#remote-debugging in Chrome once; bdg then finds it through DevToolsActivePort of the default profile (--user-data-dir <dir> reads another profile, --chrome-ws-url <port | http://host:port | ws://...> gives the endpoint). Without tab it only lists the tabs (JSON data.tabs: [{ index, targetId, title, url }]). With tab it starts a session on that tab, never reloads or navigates it, replays console messages, exceptions and Log entries from before the attach, and reports downloads (JSON data.attached: true). bdg stop only detaches; the tab and Chrome keep running. Earlier network requests are not available.
An attach session dismisses JavaScript dialogs by default (your real browser: an accepted confirm() could submit real data); --dialog accept|dismiss changes it, and an action's own --dialog still wins. A tab already on Chrome's error page attaches with ⚠ Page failed to load (data.failedLoad).
Exit codes: 83 no tab matches (the tab list is shown) or the DevToolsActivePort file is missing, garbled or stale, 81 several tabs match, 82 the "Allow remote debugging?" prompt was declined, 102 it was not answered in time.
bdg status # Page, PIDs, port, viewport, color scheme, activity
bdg status --verbose # Also Chrome executable, mode and profile
bdg status --json # { active, pageState, activity, autoStopAt?, ... }
bdg page info # Just the URL and title (--json: data.url, data.title)
bdg status --include-sensitive # URL tokens as captured (user:pass@ still removed), with a warningbdg status starts with Session active: <url> — <title> and shows the viewport and color scheme the page renders with:
Session active: https://example.com/ — Example Domain
...
Viewport: 1920×993
Color scheme: prefers-color-scheme: dark (from the system setting)
Page URLs are shown without credentials (v0.17.0), in every command and in daemon.log: user:pass@ is removed and credential query/fragment parameters (token, access_token, id_token, code, sig, key, secret field names) are shown as •••• (http://app.test/cb?code=••••&state=ok). status, sessions, page info, page tabs, network list and peek take --include-sensitive to show the parameters with a warning; user:pass@ is never shown.
When the session has downloaded files, bdg status and bdg peek list them (Downloads: 2 (last: report.txt → …, completed); JSON activity.downloads / data.downloads).
Without a session: { "active": false }, plus lastSession (reason, endedAt) when the last session ended on its own (Chrome crashed, page closed, --stop-after reached). Exit code 0 either way.
When the page's renderer crashes, the session stays up but the page cannot answer. bdg status, peek, console and network list start with ⚠ The page crashed at 18:42:10 (renderer gone); bdg page reload brings it back (JSON pageState.crashedAt, pageCrashedAt). Commands that need the page exit 107 (PAGE_CRASHED) at once. bdg page reload (or page navigate) brings the page back.
bdg stop # Close Chrome (if bdg launched it) and end the sessionbdg stop returns once the daemon has exited (up to 3 s; after that it warns and still exits 0). A Chrome attached with --chrome-ws-url keeps running.
bdg cleanup # Remove files left by a crashed session, close its orphaned Chrome
bdg cleanup --force # Kill a stuck session (daemon + its Chrome), then clean up
bdg cleanup --remove-output # Also remove a legacy session.jsonCleanup also removes atomic-write leftovers (temporary files of interrupted session-file writes) with the session files. Neither touches Chrome processes bdg did not launch. Downloaded files are kept (Downloads kept: N files in <dir>, JSON downloadsKept); --purge deletes a named session's directory, downloads included.
bdg install-skill # ~/.claude/skills/bdg and ~/.agents/skills/bdg
bdg install-skill --claude # Only Claude Code (--agents: only ~/.agents/skills)A copy that differs from the one installed (older, or edited by you) is kept as SKILL.md.bak next to it (JSON backup). When one target cannot be written (exit 82), the other is still installed.
Several agents on one machine can each run their own session, with its own daemon, Chrome, profile, port and files (~/.bdg/sessions/<name>/).
bdg localhost:3000 --session agent-1 # Start a named session
BDG_SESSION=agent-2 bdg localhost:5173 # Same, via environment
bdg --session agent-1 peek # --session goes before or after the subcommand
bdg sessions # List sessions: name, state, port, PID, URL
bdg stop --session agent-1 # Acts on agent-1 only
bdg cleanup --session agent-2 --purge # Clean up and delete its directory (profile, logs)SESSION STATE PORT PID URL
(default) active 9222 98524 https://example.com/
-
--sessionwins overBDG_SESSION; without either, the default session is used as before - Names are 1-40 letters, digits,
-or_, start with a letter or digit, and are lower-cased (ALPHAisalpha) - Without
--port, a named session takes the first free port from 9223 up -
bdg sessionsalso listscrashed(Chrome still running) andstale(files left) sessions with their cleanup command, and sessions that ended withoutbdg stop(Chrome crashed or closed, page closed,--stop-after) asended, with why and when (JSONendReason,endedAt) until they start again orbdg cleanupruns -
--purgeneeds--session
bdg page info # URL and title of the session page
bdg page navigate https://example.com/next # Load a URL and wait for it
bdg page reload
bdg page back # History back / forward
bdg page forward
bdg page navigate example.com --no-wait # Return at once
bdg page navigate example.com --json # { action, url, title, status }
bdg page emulate --viewport 900x700 # Change the viewport mid-session (responsive checks)
bdg page emulate --color-scheme light # ...or prefers-color-scheme; --reset clears both
bdg page emulate --mobile # A phone (390x844 unless --viewport), touch, mobile user agent- Reports the document's HTTP status and warns on 4xx/5xx (
⚠ The page responded with HTTP 404) - A URL that ends on Chrome's error page (refused, DNS, TLS, redirect loop, …): exit 80
Could not load <url>: net::…;page reloadanddom wait --loadtoo. A certificate warning page: exit 0 with⚠.page backwith no history entry: exit 81 - A page still loading after the wait gets the same
⚠ The page is still loading …warning anddata.loadingasbdg <url> -
page tabs,page switch,page closemanage the browser's tabs (see Tabs and popups) -
page emulateapplies like--viewport/--color-scheme/--mobileat start, without a reload. Screenshots andbdg statusfollow it.
bdg page tabs # Tabs (2): * [0] Shop http://localhost:3000/ (--include-sensitive: URL tokens as captured)
bdg page switch 1 # Act on tab 1 (0-based), a target id or a part of the URL
bdg page close [target] # Close a tab (index, target id, URL part, url:<part>); default: the session tabAn action that opens a popup or tab says so: Opened: popup http://localhost:3000/authorize (bdg page switch 1) (JSON opened: [{ url, targetId, kind, index }], kind popup or tab; page tabs --json has kind too). After page switch, dom commands, bdg cdp and telemetry act on that tab. When the session tab closes, the session moves to its opener (or the previous tab) and the next action result says Tab closed: …. Ctrl-C during page switch waits for it to finish, then exits 130 (143) saying whether the session moved.
bdg state save login.json # Cookies (session and HttpOnly too) and localStorage/sessionStorage
bdg state save login.json --origin https://auth.example.com # Only these origins' storage (repeatable)
bdg localhost:3000/admin --state login.json # Restore before the first navigation
bdg --session other state load login.json # Into a running session, then reload (--no-reload to skip)state load writes storage only for saved origins the page has frames of; others are listed as skipped (not-on-page: use --state at start). --state with --chrome-ws-url exits 81.
All DOM commands take CSS selectors and search the page like a user sees it: the document, open shadow roots and same-origin iframes. Three filters may be added to an element of a selector, alone or combined:
| Filter | Keeps elements whose... |
|---|---|
:has-text("text") |
text contains text (case-insensitive); also matches ancestors, so give an element selector. Empty text exits 81 |
:text-is("text") |
text is exactly text (case-sensitive, whitespace collapsed) |
:visible |
box is non-empty and visible (opacity: 0 still counts as visible) |
Hidden elements match text filters by their textContent; add :visible to leave them out. Button inputs match by their value.
bdg dom click 'button:has-text("Save")'
bdg dom click 'nav a:text-is("Home")'
bdg dom query 'li:visible'
bdg dom fill 'input[name="q"]:visible' "shoes"Scoped to a row or label. A filter on an earlier element scopes the rest of the selector to the elements that pass it, so you reach the control in the row or label holding the text. Only a descendant (space) or child (>) combinator may follow a filtered element (+ / ~ exit 81). Filters also work inside :has():
bdg dom click 'li:has-text("Write report") .toggle' # Checkbox in the row saying "Write report"
bdg dom click 'li:has(label:text-is("Write report")) .toggle' # Same, by exact label text
bdg dom fill 'label:has-text("Customer name") input' "Ada" # Input inside that label
bdg dom click 'tr:text-is("Ada Lovelace") > td button' # Button in a table rowdom fill, dom click and dom press-key on a <label> act on its control (reported as input (via label)).
Cross-origin iframes and closed shadow roots are not searched (use bdg eval --frame for cross-origin iframes). While the page is still loading, "not found" errors say so and suggest bdg dom wait.
Some pages (anti-bot scripts, old libraries like MooTools) replace browser built-ins such as querySelectorAll or JSON.stringify. dom query, get, inspect, layout, audit, wait, form, a11y and screenshot run in bdg's isolated world and are not affected. Actions run in the page's world: when the page replaced the selector search, they find the element in bdg's world and warn (--json: replacedBuiltins); when their script fails because of a replaced built-in, they exit 90 naming it. bdg eval, dom listeners and bdg cdp run in the page's world.
dom query, dom form and dom a11y query number their results from 0. Use the number instead of a selector in later commands (bdg dom click 0, layout 0, get 0). An index addresses that exact element; if it is gone (removed, page navigated), the command exits 87: re-run the query.
One cache holds the last of these lists, and the output says which one an index came from: Element: h3 "Welcome" (index 0 of the last dom query "h3") (data.indexSource). Indices from dom a11y query also work for elements in a same-site cross-origin iframe, such as a consent dialog.
--index <n> picks the n-th match (0-based) when a selector matches several elements.
bdg dom query "button" # [0] <button type="submit"> Sign in ...
bdg dom query "div" --limit 200 # List 200 matches (0 = all)
bdg dom query ".error" --json # { selector, count, nodes: [{ index, nodeId, tag, id?, preview?, inViewport?, ... }], omitted?, indexed? }- Previews are the rendered text (hidden parts, close buttons and
aria-hiddenicons left out) - Matches outside the viewport or hidden are marked
(below fold),(hidden), ...;--jsonhasinViewportfor the first 100 - The first 1000 matches are described and indexed; human output lists 50 of them (
--json: 100) with the total count.--limit <n>lists that many (0 = all). An index past the indexed matches says so and how to index more. JSON:count(all matches),nodes,omitted,indexed - Ends with one
Next:line:bdg dom get 0 (text), bdg dom get 0 --raw (HTML), bdg dom layout 0 (position) - No match: exit 83; for a single id or class the error suggests up to 3 similar ones (
Did you mean #remove-sauce-labs-backpack? (similar id on the page))
bdg dom get "h1" # [Heading L1] "Login"
bdg dom get 0 # By index
bdg dom get # No argument: the body
bdg dom get ".nav-link" --index 2 # Third match (0-based)
bdg dom get "#content" --full # All of the text (default: first 500 characters)
bdg dom get "h1" --raw # <h1>Login</h1>
bdg dom get "button" --raw --all # All matches
bdg dom get --node-id 123 # Node id from query / get --raw / a11y describeAn element with long text gets a Text: line with up to 500 characters (--full for all). An element without text says what it holds: No text; holds 1 element: iframe (see its HTML with --raw). --all requires --raw (exit 81 otherwise).
bdg dom layout "button" # Every match (first 20 listed; --json up to 100)
bdg dom layout "button" --index 2 # One match
bdg dom layout 0 # Cached query index
bdg dom layout "#save" --jsonPage: viewport 1280×720, scrolled to 0,0, document 1280×2500
3 elements match "button" (page x,y and size in CSS px):
[0] button#menu.icon "Menu" 10,10 40×40 visible
[1] button#accept "Accept" 300,60 100×30 visible covered by div#cookie-banner.modal
[2] button#save "Save" 420,1180 120×40 below fold (scroll down 500px to centre it)
-
inViewport:visible,partly(with the share, e.g.partly visible (87%); scroll up 5px to see all of it),above/below/left/rightwith the scroll that shows it, orhiddenwith the reason -
coveredBynames an overlay or sticky header over the element;clippedBya scroll container it is scrolled out of;inertandinvisible(e.g.opacity: 0 on div#menu) flag elements a user cannot use or see -
scrollBy(JSON) is the smallest scroll for a partly visible element and centres one out of view; fixed and sticky elements getoffScreenReasoninstead; a scroll-locked page (open consent or modal dialog) sayspage scrolling is lockedand names the dialog - Accounts for same-origin iframes, scroll containers,
zoomandtransform: scale()
bdg dom inspect "#save" # Styles grouped like Figma Dev Mode, hints, child tree
bdg dom inspect ".card" --tree 3 # Deeper child tree (--tree 0 for none)
bdg dom inspect "#save" --rules # Which CSS rule sets each property (file:line)
bdg dom inspect "h1" --why font-size # Every declaration of one property, the winner first
bdg dom inspect ".btn" --props color,padding --rules # Only these properties
bdg dom inspect ".btn" --props '--bs-btn-*' # Custom properties (--* for all)
bdg dom inspect "#save" --json # Figma-aligned JSON (fills, strokes, effects, sizing)button.btn.btn-primary "Export" 66.5x31 @530,207
box p 8 14
layout inline-block
parent div.actions block · in-parent l0 t0 r793.5 b26
text Arial 400 13.3/normal · color #fff · contrast 1.95 fail · align center
fill bg #9db8ff
border radius 8
hints none
-
Hints (by default): declarations with no effect, with the reason, the fix and where they are. This covers flex/grid properties without flex or grid, offsets and
z-indexon static elements, sizes on inline elements,var()of an unset custom property, and form controls in the browser's font. -
--why:✓marks the winner and✗what it beats, with specificity[0,2,0],@mediaand layer, and the winning rule as written under it.var()values are substituted, and indented lines say where each custom property is set.[animating: …]marks values read mid-transition. Rules under a@media/@supportscondition that does not apply now are listed too. -
Text and contrast describe the text a user sees (the descendant that draws most of it, also inside shadow roots). Contrast composites translucent ancestors;
≈marks a value that cannot be exact (blend modes, filters, canvas, an overlay) and says why. - Selection is the first rendered match (
--index nfor another).display: noneelements are still inspected. Secrets are never shown. - To check against a Figma design, see docs/FIGMA_DESIGN_QA.md.
bdg dom audit # All checks
bdg dom audit contrast --wcag AAA # Text below WCAG AAA, weakest first
bdg dom audit overflow # What scrolls sideways, cut-off text, scaled images
bdg dom audit layers animations # Fixed/sticky elements and running animations
bdg dom audit a11y # Alt text, control names, heading order, landmarks, ids, ARIA references- Contrast is composited like
dom inspect;--limit <n>sets findings per check (default 20) - Follow up on a finding with
bdg dom inspect <element>(e.g.--why color) -
a11yreads the computed accessibility tree (the page, same-origin frames, open shadow roots); each finding's element works withbdg dom a11y describe. It is part of the default run (--jsondata.a11y)
bdg css search -- --brand # Where a custom property is set and used (-- before text starting with -)
bdg css search "oklch(" # Rules that use oklch colors
bdg css search ".btn-primary" --limit 50 # Rules of a class, in every stylesheetSearches every stylesheet, cross-origin ones included, case-insensitive. Each match shows the rule (long minified rules cut to the selector and the declaration) and file:line.
dom click and the other actions wait only for the requests they start, not for results shown later by timers, spinners or animations. Use dom wait instead of sleep loops:
bdg dom wait "#finish" --visible # A match becomes visible
bdg dom wait ".toast" --text "Saved" # A match contains the text (case-insensitive)
bdg dom wait "#loading" --gone # Nothing matches any more
bdg dom wait 'li:has-text("Buy milk")' # Selector filters work
bdg dom wait --load # The page finished loading
bdg dom wait "#result" --timeout 30000 # Default 10000 ms✓ div#finish visible after 5.1s
The wait continues across navigations and reaches shadow DOM and same-origin iframes. A timeout exits 102 with what the page showed last and a next step:
Error: Timed out after 10s waiting for div#finish to be visible (last seen: 2 matches, none visible)
The matches are hidden; see why with bdg dom layout 'div#finish'
# Fill
bdg dom fill "#username" "admin"
bdg dom fill 0 "value" # By index
bdg dom fill "#country" "Poland" # <select>: by value or label
bdg dom fill "#tags" "a,c" # <select multiple>: several options ("" = none)
bdg dom fill "#agree" true # Checkbox: true/false, yes/no, on/off, 1/0
bdg dom fill "#upload" "./a.png,./b.png" # File input: one or more paths ("" clears)
bdg dom fill "#search" "q" --no-blur # Keep focus after filling
# Click and hover
bdg dom click "#login-btn"
bdg dom click "button" --index 2 # Third match
bdg dom click ".row" --double # Double-click
bdg dom click ".row" --right # Right-click (context menu)
bdg dom hover "nav .menu" # Hover menus, tooltips
bdg dom hover --off # Move the mouse off the page (closes hover menus)
# Keys
bdg dom press-key "#search" Enter
bdg dom press-key "input" ArrowDown --times 3
bdg dom press-key "textarea" a --modifiers ctrl # Ctrl+A (shift, ctrl, alt, meta; aliases cmd, control, option)
bdg dom press-key "body" Escape
# Submit
bdg dom submit "#login-form" # Submit with the form's default button
bdg dom submit "#login-form" --wait-navigation # Wait for the next page
bdg dom submit "#login-form" --wait-network 2000 --timeout 10000
# Scroll
bdg dom scroll "footer" # Element into view
bdg dom scroll --down 500 # Also --up, --left, --right
bdg dom scroll --bottom # Or --topBehavior:
-
dom filltypes text like a user (empties the field, thenInput.insertText): the page gets trustedbeforeinputandinputevents withinputType,changeon blur, and Chrome appliesminlength - Real mouse and keyboard input;
click/hoverfall back to DOM events with a warning when the element is covered or has no size -
click,fill,hover,press-keywait for the requests the action starts (150 ms idle, up to 2 s;--no-waitto skip);scrollwaits for lazy-loaded content. For later results usedom wait - Warnings for a selector matching several elements, a hidden/inert field, or a field behind a modal
- Disabled, read-only fields and values the browser would not accept (bad color, out-of-range range value) exit 81
-
submitrefuses a form with invalid fields or a disabled submit button (81) -
alert/confirm/promptdialogs are accepted automatically and listed in the result and inbdg console.--dialog accept|dismissand--prompt-text <text>choose the answer for dialogs an action opens (dom click "#delete" --dialog dismiss); an acceptedprompt()without--prompt-textgets the page's default value - A
clickon a submit button, orpress-key Enter, whose form fails browser validation prints⚠ Submit blocked: email: Please include an '@' …(JSONsubmitBlocked) - Secret values are masked in the output: password fields,
cc-*/one-time-codeautocomplete, and fields whose name, id, autocomplete or label text names a secret - A scroll that moved nothing warns why (
Nothing to scroll: the document is no taller than the viewport (993px))
click, fill, hover, press-key, scroll and submit report the element they hit, what changed on the page and the requests they started, so no follow-up eval is needed:
✓ Element Clicked
Selector: a:has-text("Learn more")
Element: a "Learn more"
Method: mouse events
Page: navigated to https://www.iana.org/help/example-domains (200)
Requests during the action (9):
GET iana.org/help/example-domains → 301 (589ms)
GET www.iana.org/help/example-domains → 200 (359ms)
+ 7 assets (css, js, fonts, images)
| Line | Meaning | JSON |
|---|---|---|
Element: |
The element hit, e.g. input.toggle in li "Buy milk"
|
element |
Page: |
navigated to <url> (<status>) or URL changed to <url> (same document)
|
navigation: { url, sameDocument, status } |
New text: |
Messages that appeared (alerts, aria-live, flash/error/toast elements), up to 3: "Your password is invalid!" (div#flash.flash.error)
|
messages: [{ text, element }] |
Requests during the action (N): |
Documents, XHR/fetch, WebSockets first; static assets summed up | triggeredRequests |
Download: |
A download that began during the action: Download: report.txt → /Users/me/.bdg/downloads/report.txt (completed, 15 B)
|
downloads: [{ url, suggestedFilename, path, state, bytes }] |
Page: and New text: are left out when nothing changed. Warnings replace the ✓:
-
No visible effect: a
clickorsubmitwith no DOM change, request or navigation (checked again after 300 ms) prints⚠ Element Clicked (nothing observed within about half a second: no DOM change, requests or navigation; later results are not watched: look with bdg dom query, bdg peek or bdg dom screenshot)(effect: "none", still exit 0) -
Submit blocked:
⚠ Submit blocked: city: Please fill in this field.when the browser's validation stops the submit a click or key started -
Value mismatch:
dom fillreads the value back; if the page rejected or moved it, it warnsThe field's value is "" after filling (expected "Lovelace"); the page may have rejected or moved the input, addingthe value appeared in input#first-name insteadwhen another field got it (valueMismatch: { expected, actual, movedTo? }, still exit 0) -
Other warnings (covered element clicked with DOM events, click not received) start the output with
⚠ Element Clicked (with warnings)
click and press-key say when the page was still changing as they returned: the status line ends (page still changing), a note says what was pending and suggests bdg dom wait <selector>, and JSON has settled: false with pending (exit code stays 0):
-
requests: document, fetch/XHR and script requests the action started that are still running -
navigation: a new page was still loading -
loading: a loading indicator appeared and is still shown (aria-busy="true",role="progressbar", aloading/loader/spinnerclass or id) -
domChanging: DOM changes came in bursts, and a second look 250 ms later saw at least one more, with no quiet gap over 150 ms. Time the page's main thread could not run (a long task, or timers a starved renderer runs late) is not quiet time: a timer in bdg's own world, invisible to the page, measures it. A single burst more than 150 ms old also gets the second look when the page stalled for most of the time since. Changes more than about 150 ms apart while the page could run look settled -
busy: the page did not answer within 250 ms and ran a long task (over 50 ms), or still did not answer a check 250 ms later, as with a long script right after the action. A page that answers late without a long task (a starved renderer on a loaded machine) is waited for instead. A page running an endless script is reported busy about 500 ms after the action
This adds nothing to an action's time unless the DOM looked busy (250 ms plus one read). A result that a timer renders later with nothing before it is not detected: wait for it with bdg dom wait.
In a Chrome bdg launched (headless or not), downloads are saved into downloads/ of the session directory (~/.bdg/downloads/, created 0700), never your ~/Downloads, under the suggested name ( (1), (2)… when taken). Downloads of tabs the page opens (target="_blank", window.open()) count too. A download still running when the action returns is inProgress and appears at its path once complete; ones that begin later are listed by bdg status and bdg peek. If the directory cannot be created, downloads are refused (canceled with a reason) rather than saved to ~/Downloads. --chrome-ws-url is the exception (see Attach to a Running Chrome).
bdg dom form # Most relevant form: fields, values, validation, suggested commands
bdg dom form --brief # Names, types, required, values
bdg dom form --all # Every form
bdg dom form --json # { formCount, selectedForm, forms: [{ fields, buttons, summary }] }Field numbers are indices for dom fill / dom click. No forms: exit 88 (on a page still loading: No forms discovered on the page yet; it is still loading, with a dom wait --load hint). A form inside an iframe: exit 89 with how to reach its fields.
-
Order: forms in an open dialog first (
(in dialog)), then visible ones, then hidden ones ((hidden)); indices follow this order, sofill 0is the dialog's first field -
Hidden fields are listed with the status
hidden(hidden: truein JSON) -
Readiness:
READY to submitonly when every required field and at least one field is filled; otherwise e.g.2 required fields empty: Last Name, ZiporNOT ready (no fields filled). A radio or checkbox group counts once; labels with a standalone*count as required - Only the submitting button is marked
(primary), never Cancel, Reset or Back
bdg dom listeners "#save" # Listeners that run for the element
bdg dom listeners "#save" --type click,keydown # Only these event types (repeatable)
bdg dom listeners "#save" --all # List React root listeners one by one
bdg dom listeners 0 --jsonLists listeners on the element, its ancestors (through open shadow roots), document and window, grouped by event type, with handler name, source preview and location. The handlers that run for the element come first:
Event listeners for button#save (93: 3 on the element, 74 on ancestors, 16 on document and window)
click
target button#save handleSave script 12:1:2345 [React onClick] ()=>f(e)
document document rowClicked script 5:3:12 [jQuery, delegate .row] function rowClicked() { … }
target button#save u0 script 9:1:3812 [no-op] function u0(){}
Framework roots (one line per node; --all lists each listener):
ancestor div#__next React root: 45 event types, capture and bubble (dispatchDiscreteEvent, dispatchContinuousEvent, dispatchEvent)
-
React: the
on…props React runs for the element and its React parents are listed as[React onClick](framework: "React",reactPropin JSON); React roots collapse to one line -
jQuery: handlers behind jQuery's dispatcher are shown with their name, source and delegate selector (
[jQuery, delegate .row]) - Empty placeholders are marked
[no-op]; a misspelled--type(Click,onclick) suggests the right one - No listeners is not an error (exit 0)
bdg eval "document.title" # Run JavaScript in the page
bdg eval 'document.querySelector("h1").textContent'
bdg eval 'await fetch("/api/me").then(r => r.status)' # Top-level await works
bdg eval "location.href" --json # { result, type }- Human output prints strings raw (
My Page, not"My Page"); empty,"undefined"and JSON-like strings stay quoted.--jsonis unchanged - Works like the DevTools console:
const,letandclassstay local to the call (a later call can re-declare them); usewindow.x = ...to keep state between calls - Values JSON cannot represent are described (
NaN,123n,button#submit,Map(1) {...}) - On a page paused in the debugger (a breakpoint or
debugger;with the Debugger domain on): exit 85The page is paused in the debugger, resume withbdg cdp Debugger.resume - A script that throws exits 91 (so does page code that throws during
dom click,fill,submit,press-keyorscroll); a script running over 20 s is terminated and exits 102 (the page stays usable)
bdg dom frames # [0] http://localhost:8940/frame.html name=widget same-origin
# [1] http://127.0.0.1:8941/frame.html #pay cross-origin, out-of-process
bdg eval --frame 1 'window.config' # By index (main page is not listed)
bdg eval --frame pay 'document.title' # By name or id of the <iframe>
bdg eval --frame 127.0.0.1 'location.href' # By part of the URL--frame matches an index, a name/id, or part of the name, id or URL, and works for cross-origin iframes too. Several matching frames: exit 81 (they are listed); no match: exit 83. The Frame: line goes to stderr, so stdout pipes into jq.
dom frames reports the origin a frame's scripts run with (srcdoc and about:blank frames inherit their parent's; sandboxed and data: frames are null), indents nested frames (parentIndex in JSON), and says when the page is still loading (data.readyState). A frame that navigates or is removed while a script runs exits 83.
bdg dom screenshot page.png # Full page, resized for vision models (1568 px max edge)
bdg dom screenshot view.jpg --no-full-page # Viewport; .jpg/.jpeg is JPEG, else PNG
bdg dom screenshot full.png --no-resize # Full resolution
bdg dom screenshot btn.png "#save" # One element: selector or query index (or --selector / --index)
bdg dom screenshot card.png ".card" --padding 16 # Element with page around it (shadows and outline are always included)
bdg dom screenshot foot.png --scroll "footer" # Scroll first, capture viewport
bdg dom screenshot hq.jpg --quality 100
bdg dom screenshot frames/ --follow --interval 500 --limit 10 # Frame sequence into a directory--json reports path, width, height, finalTokens, resized, captureMode ("full_page", "viewport" or "element"), ...
A capture may change the page's emulation (pixel ratio 1 on a high-DPI or --mobile page, the viewport for an element beyond it) and always puts it back, also when it fails or is interrupted; other page commands (dom, page, cdp) wait until it is back. Ctrl-C on a single capture exits 130 at once (143 for SIGTERM), with the error envelope under --json.
bdg dom a11y tree # Indented tree (first 50 nodes; --limit 0 for all, --depth <n>)
bdg dom a11y query role=button # Numbered results, usable as indices
bdg dom a11y query 'role=button name=Sign in' # Fields separated by spaces or commas; * wildcard
bdg dom a11y query 'name=E-mail address:' # A name runs to the next field (spaces, colons)
bdg dom a11y query role=link --limit 0 # List all (default: first 50; --json: all)
bdg dom a11y describe "button[type=submit]" # Or an index
bdg dom a11y "Sign in" # Quick search by accessible nameAll a11y matches are indexed, even those not listed (... and 213 more), and work with click, fill, hover, press-key, scroll, submit, layout and get.
bdg network list # Last 100 requests
bdg network list --last 0 # All
bdg network list --filter "status-code:>=400"
bdg network list --filter "domain:api.* method:POST" # AND
bdg network list --filter "!domain:cdn.*" # Negate with !
bdg network list --filter "url:access_token" # Part of the URL as shown (!url: negates, * matches anything)
bdg network list --preset errors
bdg network list --type XHR,Fetch
bdg network list --follow # Stream new requests
bdg network list --page current # Only the loaded page's requests (default for errors, failed, slow)
bdg network list --preset errors --page all # Include earlier pages
bdg network list --sort size --last 5 # 5 heaviest (--sort duration, --sort start)
bdg network list --sort duration --last 5 # 5 slowest; a request still pending ranks by its time so far (PND ≥25.0s)
bdg network list --include-sensitive # URL tokens as captured (masked as •••• by default)
bdg network list --verbose # Full URLs
bdg network list --json # { requests, totalCount, filteredCount }Filters: status-code:, domain:, method:, mime-type:, resource-type:, larger-than:, duration:, has-response-header:, is:from-cache, is:running, is:failed, scheme:, url:. domain:<host>:<port> compares the port. A filter with an odd number of " exits 81 (write "url:a b"). Operators for numbers: =, >=, <=, >, <.
Presets: errors, api, large, cached, documents, media, scripts, failed, pending, slow (see Quick Reference).
bdg network har # ~/.bdg/capture-YYYY-MM-DD-HHMMSS.har
bdg network har capture.har # Custom path
bdg network har - | jq '.log.entries | length' # To stdout
bdg network har errors.har --filter "status-code:>=400"
bdg network har full.har --include-sensitive # Keep credentials (default: sanitized)HAR 1.2 (opens in Chrome DevTools). Secret form-control values in HTML response bodies are masked too. Includes WebSocket messages. Sanitized by default, as Chrome DevTools does: credential headers, cookie values, URL tokens (?code=, ?access_token=) and credential fields of request bodies, response bodies ({"access_token":"[redacted]","expires_in":3600}) and WebSocket messages become [redacted], and names stay. Bodies are read as JSON, forms, socket.io/SockJS packets, server-sent events, NDJSON and UTF-8 base64; a whole JWT is redacted under any name. Matching is name-based (password, token, secret, passphrase, csrf, auth_code, code_verifier, camelCase userPin, ...). Only credential values are replaced, so every other byte stays (64-bit numbers too). Non-JSON syntax (JSONP, single quotes, STOMP passcode: lines) is kept as captured; log.comment says what was sanitized. Bodies bdg did not capture carry the reason as content.comment / postData.comment. Files are written with mode 0600. Export before bdg stop: data is discarded with the session.
bdg network headers # Main document request
bdg network headers <requestId> # A specific request
bdg network headers --header content-security-policy
bdg network cookies # Cookies of the current page
bdg network cookies --url https://api.example.comnetwork details and network headers mask credentials as •••• by default (cookie and Authorization values, API key/CSRF headers, URL and body tokens); a last line says so (masked: true in JSON). --include-sensitive shows them with a warning. network cookies is not masked.
network headers starts with the status line (Status: GET 404 Not Found; method, status, statusText in JSON). network list cuts long URLs in the middle so similar requests stay distinguishable (--verbose for full URLs).
bdg debug log app.js:8 total discount "cart.items.length" # lp1; each expression is its own argument
bdg dom click "#checkout" # ... logpoint lp1 app.js:8: total=25 discount=0.5 cart.items.length=2
bdg debug log src/cart.ts:7 total # Original file and line of a source-mapped bundle
bdg debug log app.js:8 total --if "total > 100" --max 5
bdg debug hits # Every stored hit (--json: structured values)
bdg debug list # id, place, state, expressions, hits, errors
bdg debug clear [id] # One logpoint, or all- Lines and columns are 1-based;
fileis the end of a script URL. A file no loaded script matches yet ispendingand binds when the script loads. -
dom click,fillandpress-keylist the logpoints that ran during the action. Values are those before the line runs; an expression that throws is reported, the page is unaffected. - The page is never paused: while a logpoint exists,
debugger;statements are skipped. At most 10 logpoints per session.
bdg console # Current page: errors and warnings, deduplicated
bdg console --list # All messages, chronological (-l)
bdg console --page all # All page loads, not just the current one
bdg console --level error # error, warning, info (includes log), debug
bdg console --last 3 # The last 3 messages (summary: the newest 3 distinct errors and warnings)
bdg console --follow # Stream new messages (-f)
bdg console --json # { errors, warnings, summary }Objects are expanded, format strings (%s, %d) are applied, and browser messages (failed loads, CORS, security) and messages from iframes and workers are included.
- The summary lists the newest 50 distinct errors and warnings, with a note for the rest;
--last <n>sets how many (0 = all) - The session keeps the newest 10000 messages;
consoleandpeeksay how many older ones were dropped (JSONdropped,totals.consoleDropped). Message indices stay stable
bdg peek # Last 10 network requests and console messages
bdg peek --last 50 # 0 = all
bdg peek --network # Network only (-n); --console (-c) for console only
bdg peek --type XHR,Fetch # Resource types
bdg peek --verbose # Full URLs
bdg peek --follow # Live view, refreshed every second (--interval <ms>)
bdg network details <requestId> # Headers, timing, body (WebSocket messages too)
bdg network details <id> --body # Only the response body, whole and raw (pipe it: | jq .)
bdg network details <id> --no-body # Headers and timing only
# Timing: queued · dns · connect · tls · send · wait · download, and the dominant phase
bdg network details <id> --body-max 2000 # Cut the body at 2000 characters (0 = whole)
bdg network details <id> --include-sensitive # Credentials as captured (masked as •••• by default)
bdg console details <index> # Message with arguments and stackThe body is cut at 20000 characters by default, in text and --json (bodyTruncated: true, bodyLength); --body or --body-max 0 gives it whole. peek marks entries of earlier page loads (previous page).
A body bdg did not capture comes with the reason (bodyNotCaptured / requestBodyNotCaptured in JSON, (not captured: …) in text): a binary or too large body, a body evicted at the 100 MB budget (request and response bodies share it), or one Chrome no longer had (Chrome no longer had the body (its network buffer evicted it, or the request was cancelled)).
Follow modes (peek --follow, console --follow, network list --follow):
- stop with exit 83 when the session they follow ends (
The session ended; stopped following); - exit 130 on Ctrl-C and 143 on SIGTERM;
- with
--json, print one compact envelope per line (NDJSON), and one error line per failed refresh.
Old command and flag names (bdg tail, bdg details …, -t/--timeout, …) still work in 0.21.0 with a warning and stop working in 1.0; see What's New in 0.20.0 for the list. peek --dom was removed in v0.8.0: use bdg dom a11y tree or bdg dom query for page structure.
bdg cdp --list # All domains
bdg cdp Network --list # Methods in a domain
bdg cdp --search cookie # Search all domains
bdg cdp Network --search cookie # Search one domain
bdg cdp Network.getCookies --describe
bdg cdp Network.getCookies # Execute (bundled method names are case-insensitive)
bdg cdp Network.CookieSameSite --describe # A protocol type: enum values or object properties
bdg cdp Runtime.evaluate --params '{"expression":"document.title","returnByValue":true}'
bdg cdp Network.getCookies --json | jq '.data.result.cookies[] | select(.httpOnly)'bdg cdp Tracing.end --collect Tracing.dataCollected --until Tracing.tracingComplete --out trace.ndjson # One shot: subscribe, call, collect
bdg cdp Fetch.enable --listen Fetch.requestPaused # Buffer events (max 1000 / 10 MB)
bdg cdp --events [<Event>] [--wait <s>] [--clear] # Read and remove (or discard) buffered events--collect <Event>[,…] subscribes before the method is sent and collects until the --until event or --timeout (default 10 s, max 120). Without --out, events are cut to about 20000 characters (omitted counts the rest); --out <file> writes them all.
Without --json, bdg cdp prints text: lists and search results one line each, --describe with parameters, return values and an example, a method's result as indented JSON (<Method>: done (no result data) when empty), errors on stderr. Scripts that parse its output need --json, which gives { data: { method, result } }. A method whose result reports a page exception (Runtime.evaluate of a throwing script) exits 91.
--describe shows enum values inline (sameSite?: CookieSameSite (Strict|Lax|None)), tags experimental parameters, and for a redirected method the one that implements it (DOM.highlightNode → Implemented by Overlay.highlightNode (redirect), with its parameters). Its example uses values that work as typed (width 1280, url https://example.com, other numbers 1, never 0).
Methods outside the bundled protocol: bdg's schema follows Chromium's tip of tree, so it can lack methods your Chrome has. A well-formed Domain.method it lacks is sent to Chrome as typed (method names are then case-sensitive), with a warning (Warning: … is not in the bundled protocol (…); sending it to Chrome as is on stderr, top-level warning in the JSON envelope). A name 1–2 edits from a bundled method or domain is taken for a typo and exits 81 with Did you mean:; --send-anyway sends it as typed. When Chrome has no such method it exits 83 and says why: an unknown domain (Unknown CDP domain Foo …, with bdg cdp --list), a name typed in one case, or a protocol redirect to a method that doesn't exist (Page.deleteCookie → use Network.deleteCookies). A few methods that would break the session are blocked and point to the bdg command to use instead (Page.close, Browser.close → bdg stop; Page.captureScreenshot → bdg dom screenshot).
Every --json output is one envelope on stdout; logs and hints go to stderr.
{ "version": "0.9.0", "success": true, "data": { } }{
"version": "0.9.0",
"success": false,
"error": "No active session",
"suggestion": "Start a session with: bdg <url>",
"exitCode": 83
}exitCode equals the process exit code. Follow modes (-f --json) print one compact envelope per line (NDJSON). See For AI Agents.
| Flag | Description |
|---|---|
-j, --json |
JSON envelope (before or after the subcommand) |
--session <name> |
Use a named session (before or after the subcommand) |
-q, --quiet |
Hide tips, "Next steps" and "Suggestions" |
--debug |
Debug logging |
--help |
Help for any command |
-V, --version |
Version (--version --json for JSON) |
--help --json |
Compact machine-readable schema: commands, options, exit codes (bdg <command> --help --json for one command in full, --full for the whole tree) |
| Variable | Description |
|---|---|
BDG_SESSION_DIR |
Session directory (default ~/.bdg; named sessions in sessions/<name>/) |
BDG_SESSION |
Named session, like --session
|
BDG_CHROME_FLAGS |
Extra Chrome flags, like --chrome-flags
|
BDG_NO_SANDBOX=1 |
Launch Chrome with --no-sandbox (set automatically in Docker or as root on Linux) |