Skip to content

harn browse

harn browse [options] <url>

Playwright-backed headless Chromium with a persistent profile and the shared cookie jar. Reach for it when you need a real browser: post-JavaScript DOM, a screenshot, console/network diagnostics, an interaction flow, or an assertion that a page actually renders the way it should. It is the Playwright sister of harn browse-ai (which returns an accessibility tree instead of DOM/HTML).

By default it writes a trio of files (<prefix>.png, <prefix>.html, <prefix>.json) for the iterate-and-inspect loop. The .json carries diagnostics: title, url, status, viewport, console events, console errors, page errors, and failed requests. <prefix> defaults to ~/.cache/harnery/browse/last; override with --out. The print-mode flags (--snapshot, --html, --json) skip the files and write to stdout instead, which is what makes the scrape pipeline harn browse <url> --html | harn read - work.

The trio’s <prefix>.html is a standalone snapshot, not a raw DOM dump. Stylesheets are fetched and inlined, fonts and images are embedded as data: URIs, and every remaining reference is rewritten to an absolute URL on the captured origin, so the file renders the same wherever it is opened later. Root-relative paths like /_next/static/app.css would otherwise resolve against whatever host serves the saved file and 404, which is what makes an unprocessed snapshot render as unstyled text. Scripts and preload hints are dropped, since neither can work in a saved page. The .json reports what happened under htmlSnapshot. Print mode (--html) is unaffected and still emits the raw serialization; a --selector capture is a DOM fragment rather than a document, so it stays raw too.

Driven Chromium keeps the selected desktop UA, client-hint headers and navigator.platform consistent. This includes the first document of a popup. Headed sessions use a real 1536 by 864 window by default; the content area follows the window and the screen dimensions come from the display. Pass --viewport for an emulated QA size. Headless default remains 1280 by 800.

Plain login only changes the UA string: with nothing attached, its client hints and navigator platform remain native. Fonts, speech voices and debugger-related signals are outside the identity override. See ADR 0186.

Flag Description
--out <prefix> Output prefix for the trio (<prefix>.png, .html, .json). Default ~/.cache/harnery/browse/last.
--no-screenshot Skip the .png (DOM + JSON only).
--no-full-page Capture only the viewport, not the full scrollable page.
--snapshot Print body innerText to stdout (skips file writes).
--html Print raw outer HTML to stdout (skips file writes; pair with harn read -).
--json Print the full JSON envelope to stdout (skips file writes).
--selector <css> Scope --html / --snapshot to one element.
--viewport <preset|WxH> mobile (390×844), tablet (820×1180), desktop (1280×800), hd (1920×1080), or explicit 1920x1080. Default desktop.
--color-scheme <scheme> Emulate prefers-color-scheme for the whole session: light | dark. Renders theme-aware pages in that scheme without page-specific toggles. Absent: the browser default (unchanged behavior).
--network-har <path> Record network traffic to a HAR file (finalized on close).
Flag Description
--click <selector> Click this selector after navigation.
--fill <selector=>value> Fill an input, e.g. input[name=q]=>hello. The separator is => (not =) so attribute selectors don’t collide.
--press <key> Press a key after navigation/fill (e.g. Enter).
--wait-for <selector> Wait for this selector before capturing output.
--evaluate <js> Run JS in the page context after navigation; the result is printed to stdout and recorded in the envelope under eval.
--batch <steps> Run multiple steps in one session, semicolon-separated (see Batch flows).
--collect [preset] Scroll an infinite feed and collect its items as JSON (see Scroll and collect).
--wait-until <strategy> Navigation wait strategy: load | domcontentloaded | networkidle | commit. Default load.
--timeout <ms> Navigation timeout in milliseconds. Default 30000.
Flag Description
--login Open the URL in the installed Google Chrome with nothing attached so a person can sign in; under WSL, the Windows host’s Chrome. Close the window or create the --login-close-file when done; later browse and browse-session runs reuse the cookies.
--attached With --login: open an automation-controlled Chrome that this process and --control-file can drive. Sites can detect it and may disable the account, so use it only for agent-driven headed sessions on sites you own, never to sign in to a third-party account.
--login-close-file <path> With --login, keep the process open until another process creates this signal file, then remove it and close cleanly.
--control-file <path> With --login --attached, publish an owner-only descriptor for repeated harn browse-session control.
--headed Headed mode for a one-off (no auth-flow framing).
--browser-channel <name> Browser to launch: chrome, chrome-beta, msedge, or chromium. Headed launches default to the installed Google Chrome with automation signals hidden; chromium forces the bundled build. HARNERY_BROWSER_CHANNEL sets the machine-wide default.
--user-agent <ua|auto|native> User agent to present. Default: a normal Windows (macOS on a Mac) Chrome string at the launched Chrome’s major, stored at ~/.cache/harnery/user-agent.json and reused by browse, browse-session, and fetch; never Linux, never HeadlessChrome. A string rewrites the store, auto refreshes it, native uses the browser’s own. HARNERY_BROWSER_UA overrides per environment.
--browser-arg <flag> Extra Chromium launch flag, passed straight to the browser (repeatable). Also settable machine-wide via HARNERY_BROWSER_ARGS (whitespace-separated). See WSLg note below.
--proxy-from-env Pass HTTPS_PROXY/HTTP_PROXY to Playwright as an authenticated browser proxy. Credentials remain in the child environment instead of command arguments.
--no-cookies Skip cookie-jar attach and persist. Also skips a host extraCookies hook.
--no-pace Skip the human-pace gap between page loads of the same site for this run. See Human pace.
--store <path> Cookie store path. Default ~/.cache/harnery/cookies.json.
--profile <dir> Persistent Chromium profile dir. Default ~/.cache/harnery/browser-profile.
--export-cookies <path> Export the live profile to an owner-only Netscape cookies.txt file before closing.

--proxy-from-env reads HTTPS_PROXY, https_proxy, HTTP_PROXY, then http_proxy. It separates userinfo from the proxy server URL and passes the credentials through Playwright’s native proxy option, so Chromium can answer a Basic-authentication 407 without showing a prompt. The secret is never part of the harn browse argument vector.

When a host wrapper sets both HARNERY_BROWSER_PROXY_EXPECTED_IP and HARNERY_BROWSER_PROXY_CHECK_URL, browse opens the HTTPS check URL first and requires its ip, query, or origin value to match exactly. A missing field, bad response, or mismatch stops the command before the requested URL opens. Proxy-enabled launches also set Chromium’s WebRTC IP handling policy to disable_non_proxied_udp. Playwright does not fall back to a direct connection when its configured proxy becomes unavailable.

Terminal window
HTTPS_PROXY='http://user:password@proxy.example:7000' \
HARNERY_BROWSER_PROXY_EXPECTED_IP='192.0.2.10' \
HARNERY_BROWSER_PROXY_CHECK_URL='https://ip.example/json' \
harn browse --proxy-from-env https://example.com

Keep fixed-route checks in the host wrapper when it owns the proxy inventory. For example, a wrapper can validate the route with curl, inject the same expected IP into the child, and let Chromium repeat the gate before target navigation.

Sign-in uses plain --login. Nothing is attached to that window, so the site sees the same browser a person uses every day. An automation-controlled window is detectable no matter which launch flags hide it, and some sites answer one by disabling the account that signed in through it.

For an agent-driven visible flow on a site you own, pair --login --attached with either --control-file or --login-close-file. The owner-only control descriptor lets repeated browse-session processes inspect and act on the original Playwright context. This avoids relying on an operating-system window handle while the operator can still use the visible tab directly.

The signal-file form remains useful when another trusted control surface drives the window. A second process creates the signal only after the flow is complete; browse removes it, exports cookies if requested, persists the profile, and closes Chromium. The signal must not exist at launch, which prevents a stale file from closing a new session immediately.

Terminal window
harn browse --login --attached --login-close-file /tmp/auth-complete https://example.com
# From a second process, after the visible flow is complete:
touch /tmp/auth-complete

For live CLI control, create a private directory and launch with a descriptor:

Terminal window
install -d -m 700 ~/.cache/harnery/live-session
harn browse --login --attached \
--control-file ~/.cache/harnery/live-session/control.json \
https://example.com

Harnery reads pages at human pace by default. Consecutive loads of the same registrable site (www.example.com and example.com count as one site) wait a random 3 to 9 seconds, the way a person reads one page before opening the next. The reservation ledger at ~/.cache/harnery/pace.json is shared by every process, so a shell loop of separate harn fetch calls is paced the same as one long harn browse session, and concurrent agents queue behind each other instead of firing together. Different sites never wait on each other. Loopback and private-network addresses, single-label hosts, and the .localhost, .local, .test, .internal, .example, and .invalid suffixes are exempt, so local development and page QA keep machine speed. A wait is logged to stderr as paced 5.2s before example.com (human pace; --no-pace or HARNERY_PACE=off skips).

Control Effect
--no-pace Skip the gap for this run.
HARNERY_PACE=off Disable the gate machine-wide.
HARNERY_PACE_MIN_MS, HARNERY_PACE_MAX_MS Change the gap window. Defaults 3000 and 9000.
HARNERY_PACE_EXEMPT Comma-separated host suffixes that never wait, for sites you operate.
HARNERY_PACE_LEDGER Ledger path override.

harn browse-ai, harn fetch, harn webview, and the harn browse-session navigation verbs share the same gate and controls. Embedding hosts can pace their own page loaders through harnery/lib/pace. Decision record: ADR 0184.

These assert on the rendered layout and surface findings in the JSON envelope; each --*-fail variant makes the check exit non-zero, so they slot into deploy/CI scripts. By default each check also annotates the screenshot with colored boxes.

Flag Description
--check-visible <selector> Occlusion check: samples an n×n grid inside the element’s rect and reports visibleRatio plus the dominant occluder. Repeatable.
--check-visible-threshold <num> visibleRatio below which a target counts as occluded (0–1). Default 0.9.
--check-visible-fail Exit non-zero if any target falls below threshold.
--check-visible-sample-grid <n> Grid size for occlusion sampling (n×n points). Default 3 (9 samples).
--no-check-visible-annotate Skip drawing target/occluder boxes (JSON still emitted).
--check-width <selector> Assert the selector’s rect width is at least --check-width-threshold of the viewport. Reports viewportFill + parentFill. Repeatable.
--check-width-threshold <ratio> viewportFill below which a target counts as too narrow (0–1). Default 0.9.
--check-width-fail Exit non-zero if any width target falls below threshold.
--no-check-width-annotate Skip drawing width-check boxes.
--check-overflow Assert no horizontal overflow (document.scrollWidth <= window.innerWidth); surfaces protruding elements.
--check-overflow-fail Run the overflow check when needed, then exit 2 if horizontal overflow is detected.
--no-check-overflow-annotate Skip drawing overflow annotations.
--check-runts [selector] Scan text blocks for runts (a single word alone on a block’s last visual line), counting words via per-word Range rects. Optional selector scopes the sweep.
--check-runts-min-chars <n> Minimum block text length to scan. Default 40.
--check-runts-fail Run a document-wide check when needed, then exit 2 for a runt, missing scope, or incomplete sweep.
--no-check-runts-annotate Skip drawing runt boxes.
--check-align <selector> Compare rendered child-content centers inside a container. Uses text fragments, then descendant SVGs, then border boxes. Repeatable.
--check-align-axis <axis> auto, x, or y. Auto checks y-centers for rows and x-centers for flex columns. Default auto.
--check-align-threshold <px> Maximum center drift in CSS pixels. Default 2.
--check-align-fail Exit 2 when an alignment target fails, is unknown, or is missing.
--no-check-align-annotate Skip alignment annotations.
--check-gap <selector> Compare adjacent rendered gaps inside a container. Groups of 3+ infer their median gap. Repeatable.
--check-gap-axis <axis> auto, x, or y. Default auto.
--check-gap-expected <px> Supply the expected gap. Required to make a two-item group conclusive.
--check-gap-threshold <px> Maximum gap deviation in CSS pixels. Default 2.
--check-gap-fail Exit 2 when a gap target fails, is unknown, or is missing. A two-item group needs --check-gap-expected to produce a conclusive result.
--no-check-gap-annotate Skip gap annotations.
--check-clip <selector> Check every matching container for descendant boxes or text paint leaving its rectangular clipping chain or nearest block parent. In-flow boxes are checked horizontally against that parent even when CSS overflow is visible. Repeatable.
--check-clip-threshold <px> Allowed rectangular overrun in CSS pixels. Default 0.
--check-clip-fail Exit 2 when rectangular clipping fails, is unknown, or the selector is missing.
--no-check-clip-annotate Skip clipping annotations.
--check-overlap <selector> Detect area intersections among measurable in-flow siblings. Repeatable.
--check-overlap-threshold <px> Allowed overlap depth on either axis. Default 0.
--check-overlap-fail Exit 2 when an overlap target fails, is unknown, or is missing.
--no-check-overlap-annotate Skip overlap annotations.
--check-crowd <selector> Flag adjacent card panels (a full border, a modest corner radius, or a box-shadow) that touch or nearly touch. Wrappers that contain panels count as peers too; separation uses the nearest face panels inside them. Walks the whole subtree, so one selector catches nested cases. Repeatable.
--check-crowd-min <px> Minimum acceptable edge gap between adjacent panel faces. Default 6. Negative separation (overlap) always flags.
--check-crowd-fail Exit 2 when a crowd target fails, is unknown, or is missing.
--no-check-crowd-annotate Skip crowd annotations.
--check-hit [selector] Check interactive target size and spacing in the document or optional scope. Repeatable.
--check-hit-profile <profile> wcag-aa uses 24px; comfortable uses 44px as a product-quality policy. Default wcag-aa.
--check-hit-fail Exit 2 on target-size fail, unknown, or a missing scope.
--no-check-hit-annotate Skip target-size annotations.

Alignment and gap checks cluster wrapped rows or columns before comparing them. They measure the rendered result rather than trusting align-items, gap, or margin declarations. Do not aim center alignment at an intentional align-items: baseline row.

Clipping is deliberately rectangular. The selector is a scope, not a single element lookup: every match is checked. Within that scope, the check measures element boxes against clipping ancestors and rendered text fragments against their nearest non-inline parent. That catches an unbreakable label painting out of a narrow card even when every element box still fits. An axis stops being checked at the first ancestor that scrolls it — including the queried container — because content the reader can scroll to is reachable by design, so a capped table or a wide code block reports nothing. Anything an ancestor inside that scroller clips is still reported, since scrolling the outer box cannot reveal it. Content inside a collapsed <details> is treated as hidden rather than clipped. The check ignores sub-half-pixel rasterization noise and normal vertical glyph overhang; neither is visible clipping. Hanging indents remain valid when their paint stays inside the block border. The check uses the rectangular padding box for border radius. Arbitrary clip-path, non-axis-aligned transforms, closed shadow roots, and cross-origin frame content are reported as unknown, not certified. Overlap covers sibling geometry; paint-order and sticky-layer obstruction remain --check-visible concerns.

Crowd sits between overlap and gap: overlap needs a real 2D intersection and gap flags uneven spacing, so a stack of cards all sitting flush passes both. Crowd targets exactly that: distinct panels with no breathing room. Peers are card-like panels (a full border, a modest corner radius, or a box-shadow) or wrappers that contain at least one panel. Separation is measured between the nearest face panels inside each peer, not the wrapper boxes, so a tall section with a card near the top and prose below does not false-fail against the next sibling, while a card-grid flush against the next card still fails. Plain blocks with no panel chrome stay out of the peer list. Table cells, list and definition rows, and divided segments carry no card boundary of their own and are skipped, as are pills, chips, and badges (corner radius at least half their shorter side). Per-pair results carry the two face panels, beforeKind / afterKind (panel or composite), the edge separationPx, the axis (x side-by-side, y stacked, overlap intersecting), and the shared parentLabel.

All requested checks run before one screenshot is captured. Fail gates are evaluated only after JSON and screenshot evidence is complete. Missing selectors and layout unknown results fail every enabled gate. The JSON still records the unknown reason so the recipe can replace an unsupported selector or supply a missing expectation.

A fail flag never succeeds without running its check. Whole-document checks can infer their scope: overflow, runts, placeholders, images, truncation, contrast, target size, and critique all run against the page when their fail flag is used alone. Selector-based fail flags cannot invent a target, so --check-clip-fail without --check-clip <selector> (and the same pattern for visibility, width, alignment, gap, overlap, and crowd) stops with a usage error before Chromium opens. --assert-fail requires --assert, and --diff-fail requires --diff.

Runt results carry scope, found, and outcome. A safety-cap truncation is unknown, not a clean result; a requested selector that matches nothing is fail.

These cover failures a human catches at a glance even when the geometry is perfect: leaked template tokens, broken images, clipped text, unreadable contrast. Each takes an optional selector (default: the whole body).

Flag Description
--check-placeholder [selector] Flag unrendered tells in the visible text: {{x}} and JS template tokens, [object Object], Invalid Date, NaN, or an element whose whole text is literally “undefined” / “null”.
--check-placeholder-fail Run a document-wide check when needed; exit 2 unless it passes.
--check-images [selector] Audit <img> for a failed load (naturalWidth 0), a still-loading image, or a stretched one (rendered aspect far from intrinsic, uncorrected by object-fit).
--check-images-tolerance <ratio> Aspect-ratio deviation above which an image counts as stretched. Default 0.1.
--check-images-fail Run a document-wide check when needed; exit 2 unless it passes.
--check-truncation [selector] Flag text actively cut off by an ellipsis or -webkit-line-clamp. Stays quiet on plain overflow: hidden.
--check-truncation-tolerance <px> Overflow past the clip box, in px, above which truncation counts. Default 2.
--check-truncation-fail Run a document-wide check when needed; exit 2 unless it passes.
--check-contrast [selector] Flag rendered text below the WCAG AA ratio (4.5:1 normal, 3:1 large) against its effective background.
--check-contrast-fail Run a document-wide check when needed; exit 2 on failed, unknown, or missing results.
--no-check-<name>-annotate Skip that check’s screenshot annotations (JSON still emitted).

Placeholder is prose-safe: bare “undefined” / “null” only flag when they are an element’s whole text, not the word inside a sentence. Do not point it at a page that documents template syntax. Contrast resolves the effective background by walking ancestors to the first opaque color; text over an image or gradient is unknown, not failed, and the check reads whatever theme the page is in, so toggle the theme with --batch to cover light and dark. Truncation is deliberately narrow (an active ellipsis or line-clamp only), so a decorative overflow: hidden does not read as a bug.

Heuristic checks only catch what you can enumerate. --check-critique hands the rendered page to a vision model for the long tail: the thing that looks off without tripping a named rule.

Flag Description
--check-critique [selector] Send the page to a vision model, tile by tile. Default tiling cuts vertical bands whose seams snap into content gaps; a selector tiles one screenshot per matching element, banding any element taller than the band budget internally.
--check-critique-band <px> Band height budget for tiling. Seams may move up to 30% above the budget to land in a gap, never below it. Default 1400.
--check-critique-overlap <px> Vertical overlap below a seam that had to cut through content, so a straddling finding stays visible. Clean seams (nothing straddles them) carry a 16px margin instead. Default 120.
--check-critique-max-tiles <n> Cap on tiles sent to the model, to bound cost. Default 24.
--check-critique-rubric <text> Override the default rubric.
--check-critique-fail Run a whole-page critique when needed; exit 2 unless it conclusively passes.

The page is tiled because a tall screenshot downscaled to a model’s input budget loses the detail the critique depends on; each tile is captured at full resolution and judged on its own, and findings carry their tile and scroll offset for locality. Band seams are content-aware: the page’s text lines, images, and small bordered boxes are extracted first, and each cut lands in the cheapest nearby gap rather than at a fixed offset, so tiles rarely open or close mid-element. Where no clean cut exists (an element taller than the band budget), the cut falls back to the fixed offset with the full overlap, and the rubric tells the model to treat slice-edge clipping there as an artifact. harnery ships no model client, so a host injects a critiqueProvider into HarneryProgramContext (the extraHeaders pattern); without one, the result is skipped, and --check-critique-fail exits 2 rather than certifying a review that did not happen. A provider that throws becomes a high-severity finding for that tile rather than aborting the run.

The layout and content checks answer “does the page look right”; --assert answers “does it say the right thing”. One repeatable flag, <op> <selector> => <expected>:

Op Passes when
text the first match’s trimmed text equals <expected>
contains the first match’s text includes <expected>
matches the first match’s text matches the <expected> regex
count the match count equals <expected>, or satisfies a >= / <= / > / < comparator
exists / absent at least one / zero matches (no => expected)

For example --assert 'text h1 => Welcome', --assert 'count .card => >=3', --assert 'absent .error'. Results land under asserts in the JSON envelope (each carries the observed actual); --assert-fail exits 2 on any failure. A malformed spec reports an error and fails rather than throwing. This is the portable primitive a host layers domain assertions on (funnel totals, warehouse reconciliation).

Flag Description
--baseline <name> Save the captured screenshot as a named baseline at ~/.cache/harnery/visual-baselines/<name>.png.
--diff <name> Pixel-diff the capture against the named baseline; writes a diff PNG next to the screenshot and reports mismatchedPixels + similarity.
--diff-threshold <ratio> mismatchRatio below which the diff is a match (0–1). Default 0.01 (1%).
--diff-fail Exit non-zero if mismatchRatio exceeds the threshold.

Vision review costs the same whether you changed one word or the whole theme, unless something classifies the change first. --qa-plan compares the rendered page against a persisted baseline signature (per-element structure, attributes, direct-text digests, applied-stylesheet digests) and emits a review manifest under qaPlan in the JSON envelope before any model call is made: the change class, the stable scope selectors that contain it, the render contexts worth reviewing, and the provider-call ceiling.

Flag Description
--qa-plan Classify this render against the persisted baseline and emit the review manifest. No baseline classifies as unknown and widens to full coverage.
--qa-snapshot Persist this render (signature + DOM + full-page screenshot) as the QA baseline for the target + context, atomically. Run on a page state you trust.
--qa-target <key> Override the snapshot-store key (default: the url argument), so a production render can seed the baseline for a local route.
--qa-theme <theme> Context label for the store: light | dark (default light). Labels only; it does not switch the page theme.
--qa-state <name> Context state label (default default).
--qa-scope <selector> Explicit scope selector from the producing task (repeatable); overrides lifted anchors.
--qa-states <names> Comma-separated interaction states to review; promotes the plan to interaction-state.
--qa-reuse With --check-critique: reuse the baseline run’s verdicts for tiles whose pixels provably didn’t change (band-diff against the persisted snapshot screenshot). Only clean regions reuse; baseline findings or any pixel drift force a fresh review. Stats land under qaReuse.
--qa-reuse-threshold <ratio> Mismatch ratio at or below which a clean tile region is reused (default 0.001; lower toward 0 for stricter reuse).

The classes: text-data-only (only direct text changed, so zero vision calls), local-visual (structural changes contained by stable anchors, so review scopes to them in light contexts), interaction-state (explicit states requested), large-structural (stylesheet changed, unprovable boundary, or multi-region change: the full desktop/mobile and light/dark matrix), and unknown (no or truncated baseline: widened, never treated as text-only). Ambiguity always widens. A one-line edit to a linked stylesheet changes zero DOM bytes and every pixel, so DOM equality alone never proves a change is text-only; stylesheet digests (external sheets fetched and hashed) must match too. Stable anchors are unique #ids or data-qa-scope attributes; nested anchors dedupe to the outermost root so a container is reviewed once, not per descendant.

Reuse closes the loop: --qa-snapshot on a critique run persists the verdicts with the screenshot, and the next --check-critique --qa-reuse run pixel-diffs each tile’s document rect against that baseline and skips the model call for clean, finding-free regions that match at or below the strict threshold. Reuse can produce false re-reviews, never false passes: a region with baseline findings is always re-reviewed, a rubric or contract change is a whole-run miss, and a partially-reused run is never persisted as the next baseline.

--review-pack is the capture half of the capture-then-judge split behind harn review-pack and harn qa-run. The page is rendered once; its full-page screenshot, critique tiles (as PNG files, cut with the --check-critique-* knobs), serialized DOM, and QA signature are written into <dir>/contexts/<id>/; and the browser closes. No vision call is made. A judge reads the tiles back later, with no browser open, through the same host-injected provider --check-critique uses.

Flag Description
--review-pack <dir> Write this render into the pack at <dir>. Pair with --no-screenshot: the pack holds the full-page image.
--review-pack-context <id> Context id inside the pack (default <viewport>-<theme>-<state> from --viewport, --qa-theme, --qa-state).
--review-pack-scope <selector> Also tile one screenshot per matching element (repeatable). Scope tiles follow the full-page bands in the tile order.
--review-pack-plan Emit all native tile candidates and stable capture metadata without capturing the pack. Known gate failures still emit a plan.
--review-pack-allocation <file> Capture exactly the selected IDs from a validated context allocation. Source or geometry changes require a new plan. Page edges remain required.
--review-pack-reservation <file> Use the preliminary allocation only as a tile-count reservation. Repeat supplied gates and allocate on the current page; require unchanged full source and complete native coverage. One source-change retry is allowed. Cannot combine with allocation, expansion or inline critique.
--review-pack-hit-rect <x,y,w,h> A document-space rectangle a deterministic gate flagged (repeatable, at most 50). A band that contains one is captured as a hit band even when it lies below the tile cap, so the reviewer gets the tile that shows the defect. qa-run passes each context’s gate rectangles automatically.
--review-pack-expand <tile-id> Instead of capturing a context, re-render one existing tile of --review-pack-context at --device-scale-factor and write it beside the original as tiles/<tile>@<dpr>x.png. Existing tiles are untouched. This is the child harn review-pack expand runs.
--device-scale-factor <n> Device scale factor for the browser context (1 to 4; default 1). Every screenshot renders at that multiple of its CSS pixels.

The JSON envelope gains reviewPack (dir, context_id, tiles, scopes, coverage, and the pack-relative files; plus expanded after --review-pack-expand). The context’s coverage is the band coverage when bands were kept, else the scopes folded together.

Before the browser closes, the capture checks its own fidelity. Two probe bands (the top band and the middle band) are re-shot by scrolling to them and compared with the same region of the full-page screenshot using the band-diff mismatch threshold. When a probe disagrees, every band is re-captured by scrolling and the tiles are cut from those captures instead; the context record’s capture_fidelity says which source the tiles came from and which probes mismatched. This is how a blank block or a clipped callout that exists only in a tall full-page screenshot stays out of the judge’s findings, without any browser opening after capture.

Flag Description
--no-dev-overlay Skip auto-capture of Next.js dev-overlay issues (see Dev-overlay capture).

--batch runs several steps in a single browser session, semicolon-separated. Each step is one verb: click <selector>, fill <selector=>value>, press <key>, wait <selector|ms>, eval <js>, reload, or clipboard [<label>]. reload preserves sessionStorage and cookies, which is how you reproduce sessionStorage-restored UI state. clipboard reads the system clipboard via navigator.clipboard.readText() and records each read in the envelope under batchClipboardReads.

Terminal window
harn browse https://example.com --batch "click button.load-more; wait 1500; reload; wait 3000"

How steps split:

  • An eval step ends only at a ; outside JavaScript quotes (', ", and backticks) and outside (), [] and {}. So eval (() => { const n = 1; return n })(); wait 500 is two steps, and an arrow function with a block body needs no escaping.
  • Every other step ends at the next ;. Selectors and fill values get no quote tracking, so an apostrophe in fill input=>it's here cannot swallow the steps after it.
  • \; is always a literal semicolon and never ends a step, in any verb.
  • Two bare top-level statements in one eval (eval a(); b()) are two steps. Wrap them in a block or an arrow function.

--collect [preset] turns browse into a feed reader. Social timelines and other infinite lists are virtualized: they keep only the items near the viewport in the DOM and drop the rest as you scroll. A single capture sees ten or so items. Collect mode scrolls in steps, harvests every item in the DOM after each scroll and again during the pause, de-duplicates by key, and stops on a target or when the feed stops growing. It prints the items as JSON.

It runs inside the normal browse session, so it uses the same persistent profile, cookie jar, browser channel, user agent, proxy, and page-load pace gate. A site you signed into with --login stays signed in. --wait-for and --batch run first (to dismiss a banner or switch a tab); the trio, the checks, and --evaluate are skipped. Collect mode only scrolls, reads, and (with --collect-expand) clicks “show more” buttons. It never likes, follows, replies, or posts.

Terminal window
# 100 posts from an X search, signed in through the shared profile
harn browse "https://x.com/search?q=release%20notes&f=live" \
--wait-for 'article[data-testid="tweet"]' --collect x --collect-max-items 100 --collect-out posts.json
# Any feed, with explicit item and field extractors, one JSON object per line
harn browse https://example.com/feed --collect --collect-item "li.entry" \
--collect-field "title=h2" --collect-field "url=a.permalink@href" \
--collect-field "images[]=img@src" --collect-format jsonl

A preset is one entry in a data table: an item selector, a key extractor, field extractors, an optional “show more” selector, and text that means the site stopped serving the feed. Every part can be overridden with the flags below.

Preset Site Fields Tested
generic (default) Any feed of <article> or role="article" items text, time Yes, against a synthetic virtualized feed
x X timelines, searches, profiles, threads url, time, author, text, media[] Yes, signed in
threads Threads url, time, author, text, media[] No
bluesky Bluesky url, author, text, media[] No
reddit Reddit (current web UI) url, title, author, time, score, comments, text No
linkedin LinkedIn feeds author, time, text No
instagram Instagram home feed url, time, author, text, media[] No
tiktok TikTok web feeds and grids url, author, text No
mastodon Mastodon web UI, any instance url, time, author, text, media[] No
youtube-comments YouTube watch-page comments url, author, time, text, likes No

Untested presets were written from each site’s public markup and have not been run against the live site. Feed markup changes often. When one returns nothing, check the item selector first with harn browse <url> --html and pass --collect-item.

Flag Description
--collect [preset] Turn on collect mode. Default preset generic.
--collect-item <css> Selector for one feed item. Overrides the preset.
--collect-key <extractor> Dedupe key. Default: the preset key, else the first permalink-like link in the item, else a hash of the item’s text.
--collect-key-pattern <regex> A link must match this to count as a permalink for the default key.
--collect-field <name=extractor> Add or override a field (repeatable). name[]= collects every match as an array.
--collect-expand [selector] Click “show more” buttons in visible items before harvesting. Uses the preset selector when none is given.

An extractor is <selector>[@attr], evaluated inside each item. An empty selector or :scope means the item itself. Without @attr the value is the element’s visible text; with it, the attribute’s value, and href, src, and URL-like attributes resolve to absolute URLs. time=time@datetime reads the timestamp, text= reads the whole item, and media[]=img@src collects every image. key and firstSeenStep are reserved names.

Expansion is rate-limited to 3 clicks per step and 50 per run, with a short random pause before each click. It never clicks a link, an element inside a link, or a button whose label mentions an action such as like, follow, reply, repost, share, or subscribe. If a click changes the page URL, collect mode goes back and turns expansion off.

Each step scrolls a random 0.6 to 1.1 viewport heights with mouse-wheel notches over the feed, then pauses a random 1.2 to 3.5 seconds. About one step in eight takes a longer reading pause, and about one in twelve starts with a small scroll back up. If the wheel does not move the page, collect mode presses Page Down, then scrolls the feed’s container directly, and counts the step under scrollFallbacks. Every value is bounded and configurable, and --collect-seed makes the schedule reproducible.

Flag Description
--collect-step <min-max> Scroll distance as a viewport fraction. Default 0.6-1.1.
--collect-pause <min-max> Pause after each step, in ms. Default 1200-3500.
--collect-read-chance <p> Chance of a reading pause per step. Default 0.12.
--collect-read-pause <min-max> Reading pause, in ms. Default 4000-9000.
--collect-back-chance <p> Chance a step starts with a small scroll back up. Default 0.08.
--collect-seed <n> Seed for the schedule. The output reports the seed used, so any run can be repeated.
HARNERY_COLLECT_PAUSE_MS, HARNERY_COLLECT_STEP Machine-wide defaults for the pause and step ranges. Flags override them.

The human pace gate still spaces page loads. It does not shorten or lengthen scroll pauses, and --no-pace does not affect them.

Flag Stop reason Description
--collect-max-items <n> max-items Stop after n unique items.
--collect-max-scrolls <n> max-scrolls Stop after n scroll steps. Default 60.
--collect-max-seconds <n> time-budget Stop after n seconds of collecting. Default 300. Pauses never run past the budget.
--collect-idle <n> end-of-feed or idle Stop after n steps with no new items and no page growth. Default 5. end-of-feed means the page was at the bottom; idle means it was not, or it stopped scrolling.

Collect mode also stops with blocked when a step adds nothing and the page shows a rate-limit or login-wall message (Rate limit exceeded, Too many requests, Log in to continue, and site-specific text from the preset). Ctrl+C stops with interrupted, prints what was collected, and exits 130.

--collect-format json (the default) prints one envelope: url, preset, itemSelector, seed, stopReason, stats, and items. stats carries steps, harvests, items, duplicates (sightings of an already-collected key), expanded, readingPauses, scrollBacks, scrollFallbacks, elapsedMs, stopReason, and stopDetail. --collect-format jsonl prints one item per line and logs the summary to stderr. --collect-out <path> writes either format to a file and prints a short summary instead.

Each item is { key, firstSeenStep, ...fields }, in first-seen order. Step 0 is the harvest before the first scroll. When an item is seen again, missing fields are filled in, longer text replaces shorter text (an expanded post), and new array members are appended. Cookie values never appear in the output.

On by default. When a <nextjs-portal> shadow root is present, browse reads the queued dev-overlay issues (kind/code/message/stack) and reports them under devOverlay in the envelope. This matters because Next.js 16 + React 19 route hydration errors and most React warnings through onCaughtError into next-devtools’ error queue, not through console.error, so they never appear in consoleErrors. Pass --no-dev-overlay to skip the capture.

In print mode (--json) or the trio’s .json file, the envelope carries: url, title, status, viewport, consoleEvents, consoleErrors, pageErrors, failedRequests, and, when the corresponding flag ran, eval, har, visibility, width, overflow, runts, align, gap, clip, overlap, hit, devOverlay, batchClipboardReads, baselineSaved, and diff. failedRequests includes network and HTTP failures but omits canceled Next.js React Server Component prefetches. The trio’s .json additionally carries htmlSnapshot (stylesheetsInlined, stylesheetsLinked, resourcesInlined, resourcesLinked, inlinedBytes).

Terminal window
# Default trio: screenshot + DOM + diagnostics JSON at the default prefix
harn browse https://example.com
# Scrape-to-markdown pipeline
harn browse https://example.com --html | harn read -
# Mobile viewport, custom output prefix, viewport-only screenshot
harn browse https://example.com --viewport mobile --no-full-page --out /tmp/run
# Fill a search box, submit, and wait for results before capturing
harn browse https://example.com --fill 'input[name=q]=>hello' --click 'button[type=submit]' --wait-for 'main .results'
# Assert layout on mobile: break the build on overflow or narrow tables
harn browse https://example.com --viewport 430x932 --check-width 'table' --check-overflow --check-overflow-fail
# Run the rendered-layout suite in one browser state
harn browse https://example.com --viewport mobile \
--check-align 'nav' --check-gap 'nav' --check-clip 'main' \
--check-overlap '.toolbar' --check-hit 'header' \
--check-align-fail --check-gap-fail --check-clip-fail \
--check-overlap-fail --check-hit-fail
# Save a visual baseline, then diff a later run against it
harn browse https://example.com --baseline home-desktop
harn browse https://example.com --diff home-desktop --diff-fail
# One-time sign-in in plain Chrome; cookies persist for later runs
harn browse --login https://example.com
# Export one dedicated profile for a consumer that requires Netscape cookies.txt
harn browse --profile ~/.cache/harnery/profiles/media --no-cookies \
--export-cookies ~/.cache/harnery/exports/media-cookies.txt https://example.com