Changelog
What changed in each release, and what is on the way.
All notable changes to this project are documented here. The format follows Keep a Changelog and the project uses Semantic Versioning.
[Unreleased]
[0.13.1] - 2026-11-13
Changed
- A browser navigera starts makes two fewer requests to Google at every
start, whatever page was asked for: the list of on-device AI models
from the update server (
update.googleapis.com, sent although component updates are off:OptimizationGuideManifestBrokeris now off, and no model was ever downloaded anyway; Chrome 154 to 156 have no switch for it and still ask) and, headless, whether the user may use the address bar’s AI mode (www.google.com/async/folae, twice:AimEnabledis off, a visible window keeps its address bar as it is). A test reads Chrome’s own net log for these and for the time request 0.13.0 turned off. Still sent, and no feature of Chrome 153 turns either off (each of its feature names was switched off in turn): the account list (accounts.google.com/ListAccounts) and the messaging check-in and registration (android.clients.google.com).
Fixed
sessionson Windows could count another session’s browser as a session’s own memory (225 MiB for a session on a shared browser, in a CI run): Windows keeps a process’s parent id after that parent exits and gives the id to the next process it starts. A process younger than the one its parent id now names is no longer counted below it.
[0.13.0] - 2026-11-13
Added
- A Claude Code plugin:
/plugin install navigera --marketplace andrey-usa/navigeraadds the skill to every session. The plugin folder isskills/(its manifest, a README and the skill; no hooks, no MCP server, no scripts) and the repository is its own marketplace (.claude-plugin/marketplace.json). A test keeps the two files in agreement.
Changed
- navigera’s own page scripts (finding the element and clicking, filling
or hovering it; arming, checking and reading the change journal) are
called with the target and the values as arguments, not written into
the script text. Text that carried a selector, a value or a journal id
was new to V8 on every call, and parsing 20-40 KB of it again cost
2-4 ms each time; the same text is compiled once. The document’s
global object is asked for once per document; one from a document
since replaced is refused by Chrome before anything runs, and asked
for again.
NAVIGERA_CONST_CALLS=0writes the values into the text as before, to compare against; Lightpanda is unchanged. On CI’s runner, same run, against the commit before (bench run 43438627853): no gain there, inside the spread (fill5.9 and 5.9 ms,click11.1 and 12.1, the fill, fill, click, read batch 27.8 and 28.8): V8 parses fast on that machine and the bench repeats one selector. The gain is on a slower machine and for a caller whose selector or value is new each call. - A page script a document calls a second time is kept there as a
function object and called by its id from then on: no text is sent,
hashed or looked up (a 40 KB function sent as text cost about 0.8 ms
a call even when V8 had it compiled). A page acted on once pays
nothing for it. With the round trips below, on CI’s runner, same
run, against the commit before them all (bench run 43491069940):
the fill, fill, click, read batch 39.4 to 26.3 ms,
fill7.1 to 6.9,click13.9 to 13.2, a submitting click 13.1 to 10.5 (same output). - An action makes fewer round trips to the browser; each cost about half a millisecond whatever it did. A click after a look: 20 commands to 13; a fill: 6 to 5. The press’s arrival, the navigation’s verdict and the first look at what changed come back together; the URL, the title and what the page says for the history entry are one read; the journal’s prime is written ahead of the action and read when its answer is wanted; a patched read asks for the document’s listeners with the node count, keeps the document between reads, asks for the changed roots behind the delta read and describes them all at once; object releases are not waited for; the turn of the page’s event loop a change report waits for is asked for ahead of the state read and shares its round trip. Measured with the kept functions above (bench run 43491069940); the batch’s output is the same.
- A browser navigera starts no longer asks Google for the time
(
clients2.google.com/time) nor opens a connection to the search engine (www.google.com) at every start, whatever page was asked for:NetworkTimeServiceQueryingandPreconnectToSearchare off. Read from Chrome’s own net log (--log-net-log) on a local page, Chrome 153. Still sent by the browser itself at a start, each to be switched off once a flag that does it is found: the account list (accounts.google.com/ListAccounts), a messaging check-in (android.clients.google.com/checkin), a component update check (update.googleapis.com) and the search engine’s AI mode eligibility (www.google.com/async/folae). - A click on a page where nothing can move the element no longer waits
a frame to see that its box holds still: the top document, loaded
with its fonts and images, unchanged for 100 ms, no short timer
queued since the action was armed, nothing animating, and no scroll
needed. After an action that repainted (a field filled just before,
in a
batch) that frame was a whole refresh interval away. Any other page waits as before, and a press that lands on something else is still held back and sent again. On CI’s runner, same run, against the commit before, the page rested 150 ms (bench run 43393635223): the bench’s fill, fill, click, read batch 60.6 to 44.1 ms median. A page that changed 5 ms earlier is not still, and waits as before. Not on macOS, where a browser whose page has been still a while is slow to draw its next frame: the wait took that delay before the press, and without it the page’s own INP counted it (a button with no handler read 304 and 328 ms invitals, in 2 of 7 CI runs). - A browser navigera starts gets two more flags,
--disable-search-engine-choice-screenand--hide-crash-restore-bubble: a visible window no longer shows the search engine choice of a fresh profile or the “restore pages?” bubble over the page. A headless start is as fast as before. - The operations bench rests the page 150 ms after each state reset, for every tool, untimed: an agent’s call comes a model turn after the page last changed, not 5 ms after.
- A compact
batchspends fewer round trips on work it repeats per step: a step whose report is owed to a later one no longer waits a turn of the page’s event loop for a request it may have started (the last action settles once); the history entry takes the page’s URL from the result the op already read; a click after a fill skips the journal prime the page would answer at once; and the crash record re-lists the browser’s tabs only when a tab opened, closed, or navigated (apushStatecounts). The same four steps on the 500-card page: 48 to 39 CDP commands, the JSON batch 33 to 26; on CI’s runner, same run, against the commit before (bench run 43369682011): 53.2 to 46.4 ms median. The report, the history, and the crash record read the same. fillchecks that a field can be typed into and fills it in one call to the page instead of two, when the field is ready at once. A field that is not ready yet (hidden, disabled, read-only) is waited for as before and found again after the wait, since a page can replace a field as it enables it. On CI’s runner, same run, against the commit before (bench run 43383430698): onefill7.3 to 6.2 ms; the bench’s fill, fill, click, read batch flat (58.7 and 59.3 ms).
Fixed
- After a shared browser crashed, a tab could be reopened at the page it had left: the record of the tabs’ URLs was renewed when a navigation was requested or started loading but not when it committed, which is when the tab’s URL changes, and it was read before the event watcher had caught up with the connection. A command that ran while a slow form POST was on its way kept the old URL for good. The stamp now also moves at the commit, and while the watcher is behind the tabs are listed again instead of trusted (waiting for it would slow every command). The form POST crash test, six copies at once: 3 of 48 failed before (the tab came back at the earlier page), 0 of 60 after. Not in a release: the record was renewed on every command up to 0.12.0.
webmcp-callon Chrome 159 calls the tool by the id its registration has there, not by name: Chrome 159 deprecates the name, which calls whichever tool holds it when the command arrives, and a page may have replaced the tool sincewebmcp-listshowed it. A tool the page took away is dropped from the list by that id too. Older Chrome, which has no id, is called by name as before.
[0.12.0] - 2026-11-12
Added
-
http [METHOD] <url> [--json … | --body … | --form k=v] [--header …](MCP: abrowser_runstep{"op": "http", …}): one request outside the page with the cookies the browser holds for its URL, HttpOnly ones included and never printed; the cookies the answers set go into the browser. For an API the page can’t call: another origin without CORS, or a POST a link can’t make. Redirects are followed one hop at a time, each checked against--allow-domains; anAuthorizationorCookieheader written for the first URL is not sent on to another origin, and an answer can set cookies only for its own host or a parent of it, as in a browser. In the agent eval’sapitask (DeepSeek-V4.1-Flash), 56 runs with it against 32 without (runs 43173307176 and 43239617655 without; 43173999730, 43239662822 and 43250943497 with): used in 43 of 56; median turns 14 to 13 and wall time 37 to 31 s, both 95% intervals clear of zero; tokens -11% and estimated cost -6%, inside the noise; the session id in the model’s context in 12 of 32 runs without, 6 of 56 with. Alone, the confirmation run on this build (24 runs) shows -1 turn with an interval touching zero. The binary grows 0.4 MB (the TLS stack was already in it). -
--attach wss://…behind a proxy: the connection goes through$HTTPS_PROXY(or$ALL_PROXY) as an HTTP CONNECT tunnel, by curl’s rules ($NO_PROXYskips it; credentials in the proxy’s URL, percent-encoded, sent as Basic auth and never shown). A proxy that wants credentials, turns them down or refuses the tunnel says so; anhttps://or SOCKS proxy is refused, not bypassed. -
click <target> --until-gone [--max <n>](JSON and MCPuntil_gone): a list behind a “Load more” button in one call. It clicks again while the element is there, shown and enabled and each click changes the page, at most 50 clicks: it follows the element it clicked first (a button that reads “Loading…” between pages is still the one), lets what each click fetched land, waits while it is disabled and the page is loading, and stops when the element is gone, hidden or stays disabled, a click changes nothing or opens another page, or at the limit;clicksandstoppedsay how many and why. In llm-bench’s catalog task a model wrote anevalloop of clicks and sleeps instead, got a wrong count, and spent five turns checking it. -
ax --grep "<words>": the lines of the page’s tree that mention the words, each under the containers it sits in, with refs to act on. On MDN’s element index the lines for<dialog>are 0.6 KB of a 93 KB tree; an agent that knows a word of an element’s name gets its ref without reading the page. -
text --grep "<words>": the passages of the page’s text that mention the words, each with the lines around it (--context, 1) and its line number, the first five (--limit) with a count of the rest. One call and a few hundred bytes for one fact from a long page, where an agent otherwise reads the page or writesevalaftereval. Several words match in any order when the phrase is nowhere; with--selector, inside that element. MCP:browser_snapshottakesgrep. With this andax --grep, an agent (DeepSeek-V4.1-Flash, 8 sessions a task and build) finding one fact from a large docs index took 6 and 7 model turns where the build before took 10.5 and 13.5, at 44% and 65% less estimated cost and 57% and 65% less time; small-page tasks did not change (runs 42978179657 before, 42978184533 after).
Changed
- In a compact
batch, actions one right after another report once, at the last of them: what they changed together since the last look. Each used to report its own change, reading the page again each time, and a change one step made and the next undid cost two lines that said nothing. Two fills, a click and anevalon a 500-card page after a look (40 runs each, interleaved, local release build, Chrome 153): median 64.0 to 46.8 ms, output 74 to 16 bytes. A step that starts a request still reports at once, as does one followed by a read or a wait; entries alone (fill, type, select, check) report as before; when the last action fails, what the ones before it changed is still said. The page read once after the run equals a full read on 41 trap pages in three batch shapes. - The MCP tool list says
readOnlyHintonly for the tools that read (falseis the protocol’s default): 200 bytes less of every session’s context, withuntil_goneonbrowser_clickadded. - Glama’s image of the MCP server needs no settings: its build reads a
page through
navigera mcpwith no browser variable set, using the chrome-headless-shell it installed, and fails if that does not work, so the listing no longer asks for a browser path or a decision-model key. text --selectorprints the element’s text as it reads (innerText), astextwith no selector does for the whole page: a list reads as one item a line, where itstextContentran the items together into one word. An element that is not rendered (hidden, a script holding JSON, or any element on Lightpanda) still gives itstextContent.
Fixed
- On Chrome 159 (beta as of 2026-11-12) every
evalthat only reads ran as an action: V8 there refuses a side-effect-free evaluation that also asks to await its result unless the result is a promise (1+1was refused), so the read was run again the action’s way, which arms the page and counts as a change for the next report. A read is now asked for without the wait, and only a result that is a promise is asked for again with it. Same answers on Chrome 153 and 159; the full suite passes on both (373 tests). click <ref> --until-goneon a button that removes itself (a cookie banner’s “Accept all”) never returned, and the session answered no command after it: once the element was gone the loop looked for its successor by reading the ref again, which finds the same element, and went round for good. A ref has no successor; a locator’s new element must differ from the old one and turn up within the wait. Found by the agent eval (an agent’s 900 s run timed out on it); now one click,stopped: the element is gone, in 0.2 s.--attach wss://…connects: the DevTools client was built without TLS, so every browser in the cloud (their endpoints arewss://) failed with “TLS support not compiled in”. It now speaks TLS (rustls with ring, the certificate checked against the system’s store; on Linux the system bundle or$SSL_CERT_FILE), and an untrusted certificate, a refused key (401/403) or an endpoint that isn’t one (404) each say what to do. The binary grows 1.3 MB (5.9 to 7.2 MB, 0.67 MB more compressed). Anhttps://address given to--attachis refused with the fix.- A shared browser’s crash restores the cookies and localStorage of a
session whose last snapshot failed: a tab slower than the snapshot’s
300 ms limit (a loaded machine) ended the snapshots until the next
command, and the crash found none (“a snapshot none was taken”). A
failed one is tried again while the session is idle, three times. A
client that polled the session (
statusevery 200 ms) kept the snapshot from ever running; now it runs when it is due whatever comes in, andstatusdoes not count as a command.statussays how old the snapshot is (storage_snapshot_ms). --selector,--frameor--gonewith nothing after it says why: an unquoted#starts a shell comment, so--selector #itemsreached navigera as--selector. The error now says to quote the selector.- An
--attachendpoint that is not a browser’s own (a page’s/devtools/page/…URL, a relay answering in its place) failed withcreateTarget: no targetId; the error now says what the endpoint is not and which URL to give. screenshot --annotatecould come back without its labels: the capture was asked for before the browser had painted them (seen once on a busy macOS runner, ci 43302903354). The labels are now painted before the capture is taken.
Security
state saveandnetwork --harwrote their file readable by every account on the machine (mode 0644 under the usual umask): the saved cookies and storage, and a HAR’sCookieandAuthorizationheaders. Both files are now their owner’s alone (0600), and one already there loses what it granted to group and others when it is written again.cookieslists an HttpOnly cookie without its value (--valuesshows it;--jsonis complete, for programs). Such a cookie is usually a session’s key: in the agent eval’sapitask, agents exploring withcookiesput the session id in their own context in 5 of 16 runs (run 43173307176 and its A/B), though none needed it. The line doesn’t name--values: told how, 3 agents of 24 asked for it.- An attach endpoint’s key stays out of sight: the values in its query
(
?apiKey=…,?token=…) and anyuser:password@are shown as***in every message, thestartnote andstatus’sendpointandexecutable;startgives the endpoint to its session server in the environment instead of on its command line, which any user of the machine can read (ps).$NAVIGERA_ATTACHgivesstart,serveandmcpthe endpoint without a command line at all. - Two files left the user’s own directories for the shared temp
directory, under names another account on the machine could guess. The
MCP server’s screenshot hand-off (
navigera-mcp-<pid>-<time>.png) was readable by all until read back; Lightpanda’s log (lightpanda-serve-<port>.log) was opened through a link planted at its name, emptying the file the link led to. Both are now innavigera-<uid>(mode 0700) and the log is never opened through a link.
Removed
- The decision model:
find,--find <description>on element commands,check "<yes/no question>", MCP’sbrowser_findandbrowser_check, the long-tree previewaxshowed while a model was on, and theNAVIGERA_DECIDER_KEY,NAVIGERA_DECIDER_URL,NAVIGERA_DECIDER_MODEL,NAVIGERA_FIND_MINandNAVIGERA_AX_PREVIEW_BYTESvariables. Measured on the same tasks with and without it, it saved no model turn and cost more: on the two large-page tasks 1.44 and 1.36 m$ a session with it against 1.14 and 1.10 without, at 6 to 7 turns either way (DeepSeek-V4.1-Flash, run 42978184533), and no gain on a model 13 times the price either (GPT-5.6 Terra, run 42978367965).ax --grepandtext --grepdo the reading it was meant to save, with no second model, key or network call.check <target>still checks a checkbox, radio or switch. Nothing on the page leaves the machine but the browser’s own traffic.
[0.11.0] - 2026-11-10
Added
vitals: the current page’s web vitals as the browser measured them, read from its own performance entries with nothing injected beforehand. LCP, FCP, TTFB and load in ms, CLS, and INP (the slowest interaction, one in 50 left out as Google counts it), each ratedgood,needs-improvementorpoorby Google’s thresholds, with the element behind LCP, CLS and INP as a CSS selector: what to fix. Chrome keeps only interactions of 104 ms or more, so when every interaction was quickerinpisnullandinp_under_msis 104.passkey on: a virtual WebAuthn authenticator in every tab, so a page’s passkey sign-up and sign-in (navigator.credentials.create()and.get()) are answered with no person, phone or security key. By default the device’s own authenticator, making discoverable passkeys with user verification;--authenticator usb|nfc|ble|hybridmakes it a security key or a phone.passkey listshows the passkeys it holds (never their private keys),passkey verify offfails the next ceremonies as a wrong fingerprint would,load,remove,clearandoffdo what they say. A tab opened later starts with the passkeys the others hold;resetkeeps the authenticator and drops its passkeys.screenshotof one element:screenshot 12 card.png(a ref),--selector, or a locator. The element is scrolled into view, inside its scroll containers too, and the image is cropped to its box: a 200x120 card was 862 bytes against the viewport’s 5.9 KB. MCP:browser_screenshottakesref.screenshot --annotate: each ref of a freshaxread outlined and labelled with its number in the image, so a model that reads screenshots acts on what it sees by number (click 12). The marks are drawn into the page for the capture only, in a closed shadow root (aria-hidden, no pointer events), and removed after it; the nextaxreads as before. MCP:browser_screenshottakesannotate.state savekeeps the passkeys too, undercredentials, in the shape Playwright’scredentials.get()gives (base64urlid,userHandle,privateKey,publicKey, withrpIdandsignCount), andstate loadputs them back: register a passkey once, sign in with it in every later session.a11y [--selector <css>] [--tags …] [--ignore …]: an accessibility audit with axe-core 4.14 (built in, Deque’s MPL-2.0 engine, unmodified invendor/axe-core), run in an isolated world so the page never sees awindow.axe. The violations by rule, worst first, each with its first elements as refs. WCAG 2.0 to 2.2 A and AA by default. The script is stored deflated (652 KB as 182 KB). With the rest of this release, the static Linux x86_64 binary is 6.19 MB, 377 KB more than 0.10.1’s 5.82 MB (ci run 42723047533, musl job).totp <ENV_NAME>: the current code of a sign-in’s authenticator app (RFC 6238), from the secret in that environment variable, its base32 key or theotpauth://URI of the site’s QR code; never from the command line. A code with under 3 s left waits for the next. For a second step:fill --label "Authentication code" "$(navigera totp SHOP_TOTP)". SHA-1 and HMAC are written out (checked against RFC 3174, 2202 and 6238 vectors): no new dependency.set cpu <slowdown>(set cpu 4;mid-tieris 4,low-end6, as in Chrome DevTools) runs the page’s main thread that many times slower in every tab, to measure or test as a phone would.set cpu offundoes it.
Changed
- An
evalthat only reads is answered in one message. It is first run with V8’s side-effect check, the way a console’s eager preview runs; when V8 shows it changes nothing, that answer is the reply, and there is no after-action arm to send or put back. A write, a statement or an error runs as before, once: V8 stops a refused expression before its first side effect, a source with an assignment or a statement skips the try, and a source V8 refused is not tried again in that session. Warmevalagainst the build just before, same machine and run: 0.335 ms against 0.457, 0.495 against 0.653 and 0.348 against 0.445 on three runners (bench runs 42874186598, 42874192290, 42874198065). go-rod in the same runs: 0.250, 0.501 and 0.409 ms on Chrome; 0.225, 0.382 and 0.232 ms on chrome-headless-shell, where navigera took 0.297, 0.411 and 0.311 ms. - An action spends less of its own time between the caller and the
page. Its after-action arm is compiled once per document and then
called (a page compiled the script on every action: about 100 us
against 4 us to run it); a reply is found in the pipe with memchr; and
a capture’s result is cut out by its braces instead of read as JSON.
The arm alone: warm
eval0.565 ms against 0.651 ms just before it, same machine and run (bench run 42782596458). With everything since 0.10.1, the eval fix below included: 0.34 ms against 0.10.1’s 0.61 ms, same machine and run (bench run 42863598577).
Fixed
-
A click on a window’s own Close button succeeds and goes on in the tab left, with a note (“this click closed its tab; tab 0 is active now”), and reports that tab’s state rather than the closed one’s (
tab 1 of 2, no title). When the window’s end overtook the click’s own answers, the click failed with “the tab closed while this ran”: 23 of 300 such clicks on a loaded 2-core machine before, 0 of 1,000 after (bitti’s Electron example failed this way on CI). When all three mouse events of a click were still unanswered as the tab ended, they were taken as refused and the click was sent again as DOM events. -
On the shared browser, a tab another session’s page opened no longer counts as something this session’s click did: a click that changed nothing could report a change while another session opened popups.
-
evalanswers in one round trip to the page again. Since 0.10.0 an action,evalamong them, waited for two more: one to arm the after-action proof before it, one to put the page’s timer functions back before the reply. Neither answer is read, and the page runs them in the order they were sent, so both are now sent without waiting. A warmevaltook 1.20 ms against 0.56 ms before the proof, on one machine in one run (bench run 42723530791). With the fix it takes 0.49 ms against 0.10.1’s 0.90 ms, again on one machine in one run (bench run 42726972697). Every other action saves the second wait too. The timing gate has a probe for it, 200 evals timed from the caller’s side: 86 ms against 0.10.1’s 172 ms (gate run 42726968329).
Security
- A malformed passkey (
passkey load,state load) or authenticator key (totp) is reported by the field it is in, never by any part of its value: an error message no longer carries characters of a private key or a TOTP secret.
[0.10.1] - 2026-11-09
Added
-
screencast start <dir> [--quality] [--size <w>x<h>]andscreencast stop: what the tab showed, as a JPEG each time its pixels change, with its time; the recording follows the session from tab to tab, andstopwrites<dir>/index.html, a player for the frames. -
clock: the page’s time at your command.clock install [--time <t>] [--paused]fakesDate,performance.now,setTimeout/setIntervalandIntl.DateTimeFormatin every document of every tab;setjumps the date,run-for 30madvances firing the timers due on the way in order,fast-forwardjumps firing each once,pause-atandresumestop and restart it. A reload or a new tab goes on from where the clock is. navigera’s own waits in the page keep real time, so a paused clock never stalls an action. -
set network slow-3g|fast-3g|4g(Chrome DevTools’ presets) orset network <latency ms> <down kbit/s> [<up kbit/s>]throttles every tab;navigator.connectionreports it. It andset offlineare one setting: the later wins. -
An
httpfeature for crates that use navigera as a library (navigera::http): one request outside the page with the cookie jar a browser keeps (saved and loaded instate save’s format), redirects followed with their cookies, forms, multipart uploads and query encoding. The client is native (ureq over rustls with ring, the operating system’s certificates, the proxy from the environment) and keeps connections alive, so a local request costs about a millisecond. navigera’s own binary doesn’t build it.
Changed
resetin a browser navigera started closes the session’s tabs and their browser context before it opens the new ones, instead of after. Both tabs’ renderers were alive for a moment at every reset, and that moment was a test run’s peak: 24 TodoMVC scenarios through bitti, the browser’s peak 1,088 -> 883 MiB (median of 5), in the same time. A browser navigera attached to (your own, an Electron app) keeps the old order, so its window is never left with no tab.- A headless browser navigera starts no longer loads the address bar’s
popups (
chrome://omnibox-popup.top-chrome/and its AI-mode page). It shows no address bar, yet each window, and so each browser context and eachreset, loaded them into a renderer of their own. A visible browser keeps them. --attach localhost:9222(a host and port withouthttp://) attaches to that DevTools address. It was read as a user data directory and failed with “no DevToolsActivePort”.start --attachon a headless browser someone started with a DevTools port says “a headless browser”, not “the user’s browser … behind theirs”, andstatussays"headless": true. The wait for the Allow prompt names no browser (“if it asks to allow the connection”): it may be Edge or another Chromium browser.
0.10.0 - 2026-11-07
Added
--auto-connect(also--autoconnect, and a bare--attach) works in whichever of your browsers serves remote debugging: Chrome, Chrome Beta, Dev and Canary, Chromium, Edge and Brave by their default profiles, then a browser started with--remote-debugging-port9222 or 9229.NAVIGERA_AUTO_CONNECT=1does the same for everystartandmcp.startsays which browser it found and how many of your tabs are open.- In your own browser
tab-listlists your other tabs too, underuser_tabs, and marks the tab you are oncurrent: true: the one whose window has your keyboard, else the one that came on screen last. Windows another navigera session on the same machine opened there are not listed as yours. tab-select currentworks in the tab you are on, andtab-selecttakes a target id fromuser_tabs. The tab stays yours:quitandresetleave it open.start --attach --tab currentstarts there and opens no window of navigera’s own. MCP:browser_tabsselecttakestarget(an id, orcurrent).quit --keep-tabsleaves navigera’s own tabs open in a browser it attached to, the onesresetopened as well.
Behaviour changes
- In a browser navigera attached to,
quitcloses the tabs navigera opened and leaves yours open, where it left them all. An idle or--parentshutdown does the same. The browser’s last tab is never closed. - navigera’s window in your own browser opens behind yours, so the window
you type in keeps the keyboard.
tab-newthere opens another such window, where it opened a tab in the window you used last.tab-selectandresetthere no longer bring a tab to the front: the tab you look at stays in front, and a tab behind it still reads, takes clicks and typing, and gives screenshots. showin your own browser brings the session’s tab to the front for you and hands it to you (quitleaves it open), where it was refused.- When the agent moves to one of your tabs, navigera’s own window closes if it was never used.
- A bare
--attachlooks for any of your browsers (as--auto-connect), where it meant Chrome only;--attach chromestill means Chrome.
Changed
- An action whose requests fetch data reports about 50 ms sooner. Once those requests are done, the wait for the page to settle ends as soon as the page has changed, drawn a frame and run a task with no request or short timer (up to 100 ms) of its own still pending; it used to wait out 100 ms of network quiet every time. A fetch the page chains from the response, at once or by such a timer, is still waited for, and one deferred past 100 ms still isn’t, as before. In a tab that is not on screen (no frames there) it waits out the 100 ms as before.
- After a look,
select,checkanduncheckprint only the changed lines when those lines already show the choice or the flipped box. They used to print{"selected":["Tokyo"]}or{"changed":true,"checked":true}above lines that said the same.
Fixed
- In a tab that is not on screen in your own browser, an action no longer waits about a second before it runs. Your browser runs timers there about once a second, and navigera’s wait for a frame before a click fell back to such a timer; it now waits one task of the page there. A click in such a tab takes about 230 ms where it took 1 s.
tab-listin the default compact output printed only{"active": n}once the tab list had been printed before; it now always lists the tabs. MCP:browser_tabslistreturned the page line without the tabs; it now lists them, a line each.
0.9.0 - 2026-11-06
Behaviour changes
navigera --help(or no arguments) now fits on one screen: 1.9 KB where it was 19.5 KB. It lists the commands an agent needs first, one line each, then the names of the rest and the flags to start with.navigera help <command>still gives a command’s details, andnavigera help --allthe full list.- MCP: the action tools (
browser_click,browser_fill,browser_type,browser_press_key,browser_select_option,browser_file_upload,browser_run) return what the action changed on the page, as the CLI’s compact output does, where they returned the full page state. Passstate: trueto get the full state back.browser_hover,browser_mouseandbrowser_scrollstill return the full state, because navigera doesn’t track what they change. - MCP: the default tool list has 13 tools and takes about 8 KB, down from
16.9 KB. The 13 tools agents rarely need (hover, tabs, screenshot, file
upload, dialog, WebMCP list and call, mouse, handoff, history, console,
network, resize) are listed when the agent calls
browser_more. With a decision model the list also hasbrowser_findandbrowser_check. - The skill is split into a 5 KB core (
SKILL.md, 18.9 KB before) and eight reference files that the agent opens when it needs them: acting, reading, inspecting, sessions, Electron, the decision model, setup and troubleshooting.install-skillinstalls all of them.
Added
start --allow-domains <list>(also$NAVIGERA_ALLOW_DOMAINSandmcp --allow-domains) gives a session a domain allowlist. navigera refuses navigation and requests to any other host at the Fetch request stage, before anyrouterule, on every redirect hop. Entries are hostnames, and*.example.comcovers the subdomains but not the domain itself. An empty or malformed list fails at startup. A refused navigation’s error names the denied host and the flag, andstatuslists the hosts it refused. Requests are paused at the browser too, so a popup (window.open,target=_blank) is refused before its first request rather than once navigera attaches to it, and in a browser the session owns, a service worker’s fetches are checked as well (strict mode).--allow-domains-docs-onlychecks top-level navigation only: it limits where the agent goes, and it doesn’t stop data leaving the page. Measured limits: the policy is by hostname, not origin, and Chrome’s Fetch doesn’t intercept WebSocket handshakes, so neither mode stops data sent over a WebSocket, or through a service worker in a shared browser.resetkeeps the policy andreset --alldrops it.- MCP: two tools,
browser_webmcp_listandbrowser_webmcp_call, listed throughbrowser_more, expose the WebMCP tools a page provides. The page writes those tools, so they are untrusted: calling one the page doesn’t mark read-only needsconfirm: true.
Changed
- Typing into a field on a large page no longer reads the whole page to
report. A
fill,typeorinsert-textthat changed nothing else the tree shows answersokstraight away (10,000 table rows: 574 to 27 ms; 1,000 rows: 63 to 12 ms, local, Chrome 153). To know that nothing else changed, navigera keeps a journal in the page from the last read. It covers the DOM of every document and open shadow root, form values a script set, popovers, a script writing a style rule during the entry, and the style rules that depend on an element’s state (:focus,:invalid,:has()and the like) in every sheet and the sheets it@imports, each checked against what it matched before the entry. When the journal can’t rule a change out (it saw one, a style sheet comes from another origin, a shadow root is closed), the page is read as before. The report is the same either way;NAVIGERA_CHANGE_JOURNAL=0reads after every entry, to compare. check,uncheckandselecton a large page skip the page read the same way. When the journal shows the action changed only checkboxes, radios or single selects, navigera rewrites just their lines as a read would print them (checked, a select’svalue=and the option marked(selected)), a radio switch and a “select all” included (10,000 table rows:check478 to 49 ms,select447 to 34 ms; 1,000 rows: 60 to 22 ms and 46 to 9 ms; medians of 10 interleaved, local, Chrome 153). The page is still read as before for a select inside another control’s label, a table cell or anything else named by its text, an option the line doesn’t list in full, and a required radio group.- A click on a large page reports in about the time it takes to read
what changed, without capturing the whole page. navigera starts from
the last read’s snapshot and reads again only the subtrees the change
journal names (10,000 table rows: a cell edited 588 to 159 ms, a row
added 1,071 to 315 ms; 1,000 rows: 74 to 42 ms; medians of 10 and 8
interleaved, local, Chrome 153). The journal names each element whose
attributes, text or children changed; each control a script set; each
element that a state-dependent rule (
:hover,:focus, …) or a rule reaching past its subtree (a sibling, a child’s place,:has()) now styles differently; each popover, dialog and<details>that opened or closed; each element animated in what the tree reads; and each element that gained or lost a click listener. The page is still captured whole, as before, after a new style sheet, a style rule a script wrote, a container query, a first-letter rule, a frame or shadow root, a change in<head>, or a change bigger than an eighth of the page. The report is the same either way;NAVIGERA_DELTA_READ=0captures every time, to compare. - A plain
axafter a read or an action is patched the same way (10,000 rows, theaxafter a click: 652 to 58 ms; medians of 15, interleaved, local);NAVIGERA_REST_PRIME=0turns that off. Pages whose style rules far outnumber their elements are the exception. On a site that ships its whole CSS bundle, as GitHub does (26,000 to 42,000 rules), walking the rules for the next read costs more than the capture it saves, so a read leaves them to the first action, as before (github.com, firstax193 to 72 ms; medians of 5, interleaved, live). The walk also stops as soon as it finds more state-dependent rules than the journal can track. - The first
axafter agotoon a large page is patched too when the client pauses between commands, as an agent does while it reads whatgotoprinted. During the pause the session captures the page once its network is quiet and keeps the snapshot, unprinted, as a read would (10,000 rows, firstax776 to 130 ms; 1,000 rows 93 to 34 ms; medians of 10, interleaved, local). It does this only for a client that paused 1 s or more before thegotoand before most of its last three commands, and only for a page whose capture fits in half of the shortest of those pauses: Chrome can’t cut a capture short, so a command that arrives during one waits for it. Astartand a firstgotosent together don’t count against an agent’s own firstgoto. When a page read ahead is left unread because anothergotocame first, reading ahead stops until anaxfollows agotoagain.NAVIGERA_PRE_READ=0turns it off. navigera mcpreads large pages the way the CLI does. The MCP server kept no change journal, so everybrowser_snapshoton a large page captured the whole page. Now a snapshot after an action, after another snapshot, or after a navigation the agent paused on is patched with what changed, and between calls the server reads ahead a page that a navigation loaded (10,000 rows: first snapshot afterbrowser_navigate839 to 137 ms, snapshot after a click 780 to 97 ms; 1,000 rows 85 to 30 ms; medians of 10, interleaved, client pausing 1.5 s). Tool results read the same as before. Between calls the server also saves the storage snapshot that a shared browser’s crash recovery restores, which it never did over MCP.- navigera’s own share of building a large page’s tree is smaller (10,000 rows, from a saved snapshot: tree 85 to 51 ms, text 49 to 26 ms, clickable elements 34 to 20 ms). It reads the snapshot into columns once, indexes per-node facts by node, and uses a faster hash for maps keyed by node number. Most of a full read is still Chrome’s capture.
- A read of a large page builds only the part of the tree it prints.
navigera used to build the whole tree from the DOM snapshot before
printing it, though the printer stops one line past its limit (2,000 by
default), so on a 10,000-row table most nodes were built, named and
indexed for nothing. Nodes are now built as the printer reaches them
(10,000 rows, from a saved snapshot: tree and text 147 to 31 ms; a
click’s report, a cell edited 292 to 219 ms, a row added 375 to 308 ms;
ax938 to 883 ms; 1,000 rows: a click 74 to 60 ms; medians of 10 interleaved, local, Chrome 153). The text is the same;NAVIGERA_LAZY_BUILD=0builds the whole tree first, to compare. Finding the clickable elements, which runs alongside, went from 37 to 8 ms on the same snapshot (the same elements on 103 saved pages). A click’s patch of the kept snapshot copies the subtrees it doesn’t touch as runs of nodes: 19 to 9 ms for an edited cell. - Actions no longer sit out fixed waits. Before saying what an action changed, navigera waited 50 ms for a request the action might have started; one turn of the page’s event loop now shows whether it started one. A click on a link or a submit button waits for a new document only when the page is loading one. It used to wait up to 150 ms when a router or a script cancelled the navigation (single-page apps, forms a script submits) or the form failed validation. A click’s mouse move, press and release go out together, with one wait for the three. What an action reports, and where it leaves the page, are unchanged.
- When an action puts up a placeholder (“Loading…”, “Saving”, a progress bar) for work the page starts a moment later (on a timer, the next frame, an effect after paint), the report waits for the page’s next change and reads again, for up to 1.5 s. Before, the report could show just the placeholder, and the agent spent a turn looking again.
Fixed
- An Electron session no longer switches to a window the app opens on its
own right after
start. When a second start-up window showed up late (on a slow machine), the first command afterstarttook it as a new tab and made it the active one, soaxread that window instead of the main one (seen on Windows and macOS CI, about 1 run in 20). Until the agent acts, navigates or picks a tab, navigera now chooses among the app’s windows asstartdoes (the largest one it shows). A window the app opens after that still becomes the new active tab. startright after a session ended because its shared browser crashed a second time now makes a new session. The ending session sent its answer first and released its socket afterwards, so astartin between reached it on its way out, saidalready_running, and the next command found no session (seen on Windows, about 1 run in 30). The socket now goes before that answer, and astartthat reaches a server leaving without an answer waits for it to go, then starts a new session. The same holds when the command arrived before the shared browser’s host had dropped its connections: the host refuses requests to a dead browser at once, and that refusal alone ends the session.axno longer shows content the page has hidden again behind a display lock: the body of a<details>that was opened and closed, or content undercontent-visibility: hiddenorhidden=until-foundafter it had been visible. Chrome keeps the boxes of such content and its DOM snapshot lists them, so the tree showed text and links nobody could see, and a click that closed a<details>reported no change. Reads are no slower: a 10,000-row read is within 1% (interleaved, local).- An action’s report no longer lists lines that only moved across the
line limit at the end of a cut tree. On a page longer than
axshows, opening a section above the cut reported the rows it pushed past the cut as removed, and closing it reported the rows it brought back as added. - A click no longer removes a form
submitor clipboard method the page installed while handling it. navigera removes the hooks it installs to watch a click only if they are still in place. - A click no longer presses whatever took its element’s place between the hit test and the press, such as an overlay that a mouse move or a timer opened just then, or an element that moves every frame. navigera keeps the whole press (down, up, click) from the page and presses again where the element is now. If it keeps missing, the click fails and says what it would have hit, and nothing is clicked. Before, the press clicked the other element and the report showed that element’s change as the click’s (3 of 40 clicks under an overlay that opened 4 to 37 ms after load pressed the overlay’s button; 0 now). Clicks on a still page take as long as before.
- When the page answers a press by moving or replacing the element (a
banner inserted above it on pointerdown, a button rendered again on
mousedown), the click now says so in
click_missed. The click event went to the parent the press and the release share, or the browser sent none, so the element’s own handler never ran. Before, this read as a plain success (compact output printedokfor the replaced button). The page saw the press, so navigera doesn’t press again. The check rides on the press-arrival check, with no extra round trip. - A text entry on a large page reported
okwhen a script had set a checkbox, a radio, a select or a field’s value (no DOM change, no event) between theaxand the entry, though a read showed the change. Form states are now recorded with the read, and if one has changed since, the page is read again. - A
batchsays what its actions changed, as each action does on its own. Withfillandclickin one batch it printed only the page state, so the agent had to read the page again to see how the batch ended. Steps that changed nothing print nothing, and a batch that changed nothing printsok. In the LLM bench the model still looks once more before it answers (search and form, 5 of 6 sessions each), so its token use didn’t change (8,716 vs 8,764 per session, 6 reps, same window). text --selector <css>leaves out the scripts and styles inside the element.text --selector bodyon a page with an inline script printed the script’s code after the text (565 characters for 78 of text on the bench’s order form).- A click that arrives before the page has wired up its element (a script
that clicks right after
goto, hydration, a handler set by a timer or by a script loaded after load) is sent again once the page wires it, where it used to reportno_effect. On a page loaded less than 10 s ago, a click that changed nothing and found no click, mouse, pointer or touch listener on the element, its ancestors, its document or window (so no script of the page ran for it) waits while the page loads scripts or data, and up to 500 ms after the click, for a listener to appear, then clicks once more (clicked_againsays how long the page took). A click that any listener heard is never sent again, because a script that recorded it may replay it later, out of sight. Goto and click in one batch, 3 reps each of 7 late-wiring pages (local, release, Chrome 153): clicked once in 0/21 runs before, 21/21 now; React 18hydrateRootand Vue 3 SSR mounts loaded after load: clicked once. A handled click costs the same (40 in a batch: 16.8 vs 16.6 ms each). A dead button on a page that just loaded reportsno_effectin 521 ms instead of 221 ms.NAVIGERA_CLICK_AGAIN=0turns it off. - A wait on something only a property shows (
wait --json a field’s value, focus or a checkbox, and assertions built on the same wait) ends when the page firesinput,change,focusinorfocusoutfor the change, where it used to end at the next check, up to 100 ms later. A wait for a field’s value after afillended 97 ms after the input event and now ends in under 1 ms (median of 20, timed in the page, Chrome 156). A change a script makes without an event (el.checked = truealone) is still seen at the next check. - A click waits for a slide that starts while it checks that the element
holds still. A dialog that appeared during those frames and slid in
could pass as still: a slide’s first frame hasn’t moved yet, and near
the end of an ease-out slide the box moves less than a pixel a frame.
The press then went out before the page’s
transitionendhandler had wired the button. The click now looks again for a running animation on the element and its ancestors, waits for it and its end event, and reads again. In the sliding-modal test with six browsers at once (4 cores), the button was pressed before it was wired in 30 of 660 runs before, and in 0 of 660 now.
0.8.0 - 2026-11-03
Behaviour changes
- An action says what it changed on the page, so an agent needs no look
after it. After an
ax, a click, fill, press, select, check, drag or upload that stays on the same document prints the lines of that tree that are new (+), gone (-) or read differently (~), with refs, read again the way thataxread it and after the data the action fetched has arrived (at most 40 lines, then a count).oknow means nothing visible changed, except that after a fill a line that newly shows the text just typed (the field itself, a live preview) is left out; before, a click that loaded results printedokwhile the page was still fetching them. Compact output only:--output json,serveand the MCP server are unchanged. - The tree is shorter on lists: a list item’s bullet is left out and its
text goes on its line (
- listitem: Blue Lamp, one line where there were three), and a list item carries nolevel(its indentation is its level). Numbered markers (1.) stay.
Changed
- MCP: seven tool descriptions say when to use the tool and what it
costs (
browser_snapshot,browser_type,browser_press_key,browser_wait_for,browser_file_upload,browser_run,browser_resize), andglama.jsonnames the maintainer for the Glama listing.
Fixed
- npm:
npm install -g navigeraandnpx navigerafailed on Linux and macOS from 0.6.5 to 0.7.1 (EACCES): the platform packages shipped the binary without its executable bit. The release now refuses a package whose binary isn’t executable, and the launcher repairs one that lost the bit. - A snapshot after a “Load more” waits for the page it loads again. Three
requests to one path at a steady pace counted as polling and were not
waited for, and an agent clicking “Load more” every few seconds is that
pace: from the third page on,
axshowed the “Loading…” button. A request that starts within a second of an action is now the action’s own. - The static Linux binaries (what
install.sh, npm, pip and Homebrew install) peaked at 19 MB in a scripted session, against 6 MB for the other builds, since 0.7.1: mimalloc committed its whole arena up front and transparent huge pages backed it with 2 MiB pages. On a machine whose memory is fragmented, the first snapshot of a large page also took three times as long. It now commits on demand: 6 MB. Warm snapshots of very large pages (2,000 rows, 13,000 elements) take 3-6% longer than with 0.7.1’s build (timing gate, median of 10).
0.7.1 - 2026-11-02
Behaviour changes
What a script or an agent relying on the 0.7.0 output or timing should know:
- The CLI prints compact output by default, not one JSON line per
command: an action that left the page as it was prints
ok, one that moved it only its own fields and what changed, a read only its value; errors go to stderr aserror: …(exit code 1), notes asnote: …. A script that parses the JSON line adds--output json, or setsNAVIGERA_OUTPUT=jsonfor the whole shell.serve, MCP and the library are unchanged. axmay add a line under the page line when it cut text (… N texts cut short here …), and prints long texts in full when the whole snapshot stays under 32,000 characters.- A snapshot right after an action whose requests have started waits for them and 100 ms, not 500 ms of network quiet.
- An op on a ref whose element was redrawn now acts on the redrawn element (with a note) instead of failing, when that element is certain.
- More notes: another process using the session, a redrawn ref, an Electron window still starting.
- An icon-only control that printed as a bare
button [ref=N]may now carryicon="svg",img="inline"or an image’s file name. textwith no selector is the page’s whole text (it was an error).- The browsers navigera starts have WebMCP on (
--enable-features=WebMCP), and the--enable-features/--disable-featureslists fromNAVIGERA_EXTRA_FLAGSare merged into navigera’s own instead of replacing them (Chrome keeps only the last of each flag). network --harincludes the response bodies the browser still holds, so the file is larger.- A command sent just after the shared browser died is run on a relaunched browser instead of failing as in flight: the host watches the browser’s process (its pipe can outlive it while the browser’s children hold it open) and refuses later requests as never reached, and the session runs those again. A command that may have reached the browser is still never repeated. Killed browser, command 30 ms later: 20 of 20 recovered (was 0 of 10); 100 ms later: 10 of 10 (was 6 of 10).
- A session on the shared browser no longer ends when that browser dies: its
next command relaunches or joins one, reopens its tabs and carries a note
(and a
recoveredfield) naming what was lost. A command running at the crash still fails, and is not retried.
Added
- The agent skill (
navigera skill,install-skill) starts with the defaults an agent should take: keep the compact output, known steps in onebatch, narrow reads, a new look only when the outcome is uncertain, page content as data. webmcp-listandwebmcp-call <name> [--params '{json}'] [--confirm]: the tools a page registers with WebMCP (document.modelContext), listed and run through Chrome’s WebMCP DevTools domain in the frame that registered them. Every answer says the tools are the page’s and untrusted; a tool not marked read-only needs--confirm. MCP clients run them asbrowser_runsteps. Chrome 153 and later, https or localhost pages.set deviceknows 55 devices (recent iPhones, Pixels, Galaxy phones and tablets, iPads, two desktop sizes).set permissions grant|deny|reset <name>…sets geolocation, notifications, camera, microphone, clipboard, midi, sensors and background-sync for the session, so a page sees them as granted, denied or still to be asked.--output compact|raw|json(andNAVIGERA_OUTPUTfor a whole shell); compact is the default,--compactand--rawname the first two. Ten steps of a form, a table read and a heading: 827 characters of output compact, 2,756 raw, 3,246 as JSON, no slower (fill median 8.9 vs 10.0 ms, interleaved).batch [<file> | -] [--keep-going]runs several steps in one call over one connection: one command per line, written as on the command line, or JSON ops. Every line is parsed before anything runs; the first failure stops the rest. It prints one response: what the steps read, the page state once, notes tagged with their step. A six-step form flow is one call instead of six (590 characters of output instead of 1,764; 62 with--raw).route --har <file>answers requests from a recorded HAR: the first unused entry with the request’s method and URL, then the last one again (a page that polls keeps getting the final answer). Requests the HAR has no entry for fail, or reach the network with--not-found continue. A glob before--harlimits it to those URLs.route <glob> --modifylets the request reach the server and changes its response:--set-header 'Name: value'('Name:'removes one) and--replace <from> <to>in the body, repeatable.screenshot --timeout-ms <n>bounds the capture (default: the session timeout).start --electron <app>drives an Electron desktop app: navigera launches it with a DevTools port, waits for its first window and adopts its windows as tabs, the ones it opens later included (they have no opener, unlike a popup).<app>is the app’s directory or main script (run with the Electron in itsnode_modules), a packaged app’s executable, a macOS.appbundle or a command onPATH;--electron-arg <arg>passes arguments to the app.quitends the app.tab-new,reset,showandhidesay why they don’t apply.navigera mcp --electron <app>gives an IDE agent the same.start --electronwaits for a window the app shows to have something to read or use, starts on the largest window that isn’t hidden (an app’s hidden helper windows are tabs, not the active one, also after a window closes), and reportselectron: {version, main_process}. Anaxof a window that still shows nothing says the app may be starting. Found on real apps: one with six windows started on a hidden dropdown; others read an empty shell for up to 20 s. An app that sets its own user agent gets its version from its main process, or null when that is out of reach. A single-instance app that exits because a copy of it is already running failsstartsaying that may be why.start --attach <port|ws://…>on an Electron app that is already running with a DevTools port drives it asstart --electrondoes: its windows are the tabs (new ones too, though no page opened them) and the session starts in the largest one it shows, instead of opening a blank tab the app has no use for.quitleaves the app running;quit --close-browserends it.tab-list(andtab-new,reset) give each tab’surlandtitle, so a tab can be chosen without selecting each one to look: an Electron app’s windows, a popup, the tabs of an attached browser.- An Electron app’s
<webview>s are tabs:tab-listmarks them"webview": true,tab-selectgoes into one to read and act in it, and the session never moves into one by itself (it is part of a window). eval --main <js>in an Electron session runs JavaScript in the app’s main process (Node’s inspector,--inspect=0at launch): an expression, or a function called with theelectronmodule, awaited. MCP’sbrowser_evaluatetakesmain: true.- CI runs an Electron end-to-end test on Linux, macOS and Windows: a fixture app that saves a form through its main process and opens a second window.
screenshot --quality <1-100>writes a JPEG at that quality instead of a PNG (a defaultshot-<time>.jpgname), for traces and reports that keep many.
Changed
-
If the shared browser crashes or is killed, each session’s next command relaunches it (one relaunch when several recover at once, under the host’s lock) instead of ending the session. The session’s tabs reopen at their last URLs (a tab whose last load was a form POST reopens with GET, and the note says so), the cookies and localStorage come back from a snapshot kept in memory (at most 500 ms behind, retaken when the session is idle),
setemulation, init scripts and the download directory are re-applied, and the note and therecoveredfield list what could not come back (history, scroll, typed text, sessionStorage, refs, logs). A second crash within 10 s of a recovery ends the session, as before. -
Two processes on one session name are told: a command from another process than the session’s last one notes it, and the first process’s next command says its tab was moved, from where to where (before, only a command that arrived while another ran got a note, and the process whose page was replaced learned nothing). On Linux the process is the agent, not the shell it runs each command in;
NAVIGERA_CALLERnames it anywhere. -
A ref outlives a re-render of its element: an op whose element the page drew again (a list redrawn, a button replaced after a state change) is done on the same element, found by the role, name and place the snapshot printed, and the note says so (
ref 17 was re-rendered: this acted on button "Save", now ref 48). When that is not certain (another element of that role and name, another URL, a navigation) the op fails as before. Remembering what the refs were costs about 5 ms on a 2,000-row snapshot. -
Long text in snapshots: a snapshot that cut anything says so under its first line, with how many texts and characters and how to read them (
… 150 texts cut short here (11,553 chars left out): …). When the whole snapshot, every text in full, stays under 32,000 characters, nothing is cut.ax --full-text(MCPbrowser_snapshotfull_text) prints every word, andtextwith no selector prints the page’s whole text as it reads. A 1,500-clause agreement: every clause in the default snapshot, the 150 long ones cut and counted on the second line; all 172,000 characters with--full-textortext, in about 45 ms. -
A snapshot right after an action whose own requests have started (a click that fetches) waits for those requests and 100 ms more, not the whole 500 ms quiet window: a click that loads data for 200 ms is read about 300 ms later instead of about 750, with the data in it. Traffic that hasn’t started yet (a page’s data after it loads) still gets the whole window, and a window set with
set network-quietholds after actions too. -
Snapshots of large pages cost well under half as much on Linux: the static Linux build allocates with mimalloc (musl’s allocator was several times slower on the small allocations a snapshot makes), the snapshot is no longer copied while the tree is built and printed, and the tree built from the DOM snapshot is made of typed nodes instead of a JSON map per node, built while the clickable elements are read from the same snapshot. 13,000 nested divs: 545 to 191 ms (Playwright’s ariaSnapshot: 159 ms on that machine); a header over a 2,000-row table: 563 to 193 ms (static Linux build, median of 32, interleaved, warm). Most of what remains is Chrome’s capture. macOS and Windows gain all but the allocator.
-
Pages of 800 to 3,000 DOM nodes are read from the DOM snapshot too, not Chrome’s accessibility tree: about five times faster (a 1,500-element page: 166-201 ms to 29-40 ms), the same text on 54 of 56 fixture and benchmark pages. Smaller pages keep Chrome’s tree. A
<canvas>has Chrome’sCanvasrole in the snapshot tree too.
Fixed
-
start --electroncould start on an app’s hidden helper window: a window made withshow: falsesays it is visible on its first load (Electron’s paintWhenInitiallyHidden), a shown one under Xvfb says it is hidden, and one whose page could not answer yet counted as shown. The main process now says which windows are visible (without it, the largest window is taken), a window that can’t answer yet counts as unknown, and the choice waits until the app has stopped opening windows for a moment. -
screenshot --no-waitright after a click that confirm()s, posts and reloads the page could go unanswered for the whole 35 s timeout: Chrome loses a capture asked for while the document is being replaced, and the next one works at once. With no answer after 400 ms the capture is asked for again and the first answer is taken: about 0.43 s there, and a capture that is only slow still answers in its own time. -
An icon-only button with nothing to name it by now says what kind of icon it is: an inline style’s background image (
img="bell.svg"), an icon component’s test id (icon="DeleteIcon"), and, failing all,icon="svg"orimg="inline"(adata:image), so it no longer reads as a barebutton [ref=N]. -
A scoped
ax(--selector, a ref) on a large page builds only the scope’s tree again, instead of the whole page’s and then cutting it: a header on a 2,000-row page 336 -> 166 ms, on 4,000 rows 605 -> 360 ms (0.6.5: about 214 ms on 2,000 rows). A large scope stays on the fast path (the 2,000-row table itself: 299 ms; Chrome’s tree takes 1.4 s).
0.7.0 - 2026-11-01
Changed
axon a large page (over 3,000 DOM nodes) builds the tree from the DOM snapshot it already takes for clickable elements, instead of reading Chrome’s accessibility tree, which costs about a millisecond per table row. The output is the same (roles, names, states, values, refs): on 22 fixture pages and on Wikipedia, Python’s and MDN’s docs, GitHub, BBC, httpbin and four QA practice sites the two trees print the same but for a handful of lines (a time input’s inner fields, very long cell names that Chrome cuts). Table of 1,000 / 2,000 / 4,000 rows, whole tree: 730 / 1,789 / 4,671 ms before, 155 / 325 / 681 ms now (Playwright’sariaSnapshot: 238 / 476 / 975 ms); with the default limit 149 / 317 / 604 ms.NAVIGERA_AX_SOURCE=chrome|dompicks the source.- Adjacent text in a snapshot shares lines of up to 300 characters instead
of taking a line per element: a page of 3,000
<div>cells went from 3,001 lines (39 KB) to 51 lines (15 KB). A control or any other element between texts still starts a new line, and no text is cut to pack a line. - A text, name or value cut in a snapshot now says how much it left out,
and a cut text says which ref reads all of it:
… (+1979 chars: get text 8). A name’s cut is noted outside its quotes. - A control with no name or text (an icon button) shows what the page
marks it with next to its
#idor.class: a tooltip attribute (data-tooltip,title, …), its icon (an icon font’s class, an SVG sprite’s symbol,data-icon), its image’s file name, or thearia-describedbytext:button .icon-btn icon="fa-trash" [ref=7]. Two icon buttons of one class no longer read the same. quit --close-browseron a session in the shared browser closes the browser with it when no other session uses it, and otherwise says how many do. It used to leave the browser to close after its grace period.
Fixed
- A click on an element that never stops moving (a perpetual drift) took the whole element wait, 5 s, before clicking where it last was; it now waits a second for it to hold still (about 1 s on such a button).
- A click on an element under a cover that stays put with controls of its
own (a cookie banner, a dialog) fails after about a second instead of
5 s, naming the cover and its controls:
covered by div#consent "We use cookies. Accept Reject" … Close it with one of its controls ("Accept", "Reject"). While the page still fetches, a cover is waited for as before: a loading overlay with a Cancel button goes when its data arrives, and the click then lands. - A session under a session directory with a long path (a deep
$XDG_RUNTIME_DIRor$TMPDIR) failed to start: the shared browser’s socket path did not fit a socket address. Such a session now takes a browser of its own, and its log says why. - A long poll (a chat or notification request held open, reconnected when it ends) no longer holds snapshots for up to 3 s each cycle: a fetch/XHR open past 1 s stops holding a snapshot, and its URL’s later requests never hold one. On a page holding a 20 s long poll: 3,022 / 1,976 / 7 ms for three snapshots in 0.6.4, 1,002 / 7 / 7 now.
- Analytics beacons at irregular intervals no longer hold the first snapshot: a request the server answers 202 or 204 (no content) is background traffic, and that method and URL’s requests, the ones still out included, don’t count, in whatever order their events arrive (first snapshot on such a page: 505 to 8 ms). The end of a long poll no longer restarts the quiet window either.
axon a large page through the shared browser lost up to a quarter of its speed in 0.6.5: each of the snapshot’s thousands of tree reads crossed four threads of the browser’s host. The host now routes a message on the thread that reads it, and renumbers and routes answers and events by their envelope alone, without parsing them. A 2,000-row table: 1,563 to about 1,400 ms (0.6.4 with a browser of its own: about 1,265).axwith the default limit reads a page the cheaper way for that page. The read that stops at the limit, node by node, now gives up for one full read when it projects more node reads than a quarter of the page’s DOM nodes, as on markup where every printed line sits under many wrappers. A 2,000-row list nested in divs: 5.1 s in 0.6.4, 3.1 s now; a 1,000-row table is read in one call (about 900 ms); 2,000- and 4,000-row tables keep stopping at the limit. A snapshot that gave up reads the page’s frames and clickable elements once, not twice.
0.6.5 - 2026-10-31
Added
drag --to-at <x>,<y>drops at that point of the target (CSS pixels from its top-left corner; default its center),--from-atpresses at that point of the source, as Playwright’stargetPosition/sourcePosition: a drop near a list row’s top edge lands before it.- Named sessions share one browser, each in a browser context of its own
(tabs, cookies, storage, cache, downloads, permissions apart), so N
parallel agents cost one browser plus a renderer each instead of N
browsers. A small host process keeps the browser on the private pipe (no
DevTools port), gives each session a browser-level CDP session of its own,
disposes a session’s contexts when it goes (killed or not) and closes the
browser 30 s after the last session (
NAVIGERA_SHARED_GRACE_S).navigera mcpshares the same way.start --own-browser(orNAVIGERA_SHARED_BROWSER=0) keeps a browser per session;--attach,--profileand Lightpanda keep theirs.statussaysbrowser: shared (N sessions)orown; if the shared browser dies, each session’s next command says so andstartrelaunches it. navigera sessionslists the named sessions (pid, age, idle time, browser, URL, memory) and the shared browsers;sessions --cleanquits the ones idle 5 min or more (--idle <s>), removes dead socket files and closes a shared browser left unused, and says how much memory that freed;--clean --allquits every session.start --parent <pid>(orNAVIGERA_PARENT_PID): the session quits when that process exits, so a crashed agent leaves no browser behind.- A command that came in while another client’s command was running on the same session gets a note: a session name is shared by every process that uses it.
Changed
- An unused session ends after 10 min, not 30 (
start --idle <s>, or--idle-timeout-s).statusandsessionsdon’t count as use. - Chrome starts with
--disable-field-trial-config, as Playwright’s and Puppeteer’s do: Chromium and Chrome for Testing builds no longer turn on the experiments of Chrome’s field-trial testing config, which cost about 19 MiB per page (8 sessions: 894 to 716 MiB) and differed from build to build. - A ref whose element has left the page fails at once (“that ref is stale … run
axfor fresh refs”, no--forcehint) instead of after the 5 s wait for a box. An element withdraggable="true"gets a ref inaxeven with no handler on it. - The decision model’s key is
NAVIGERA_DECIDER_KEY, one key for whichever modelNAVIGERA_DECIDER_MODELnames (Jev, Kev, …).TYPESAFE_API_KEYis still read when it is unset. axon a page whose tree is about as long as the output limit or shorter reads it in one call instead of node by node (a 250-row table: 342 to 163 ms), and no longer copies the reply. A page with more than 4 DOM nodes per line of limit still reads lazily, stopping at the limit.- A snapshot right after a
waitthat succeeded (text, selector, url, gone, js, idle) skips the network-quiet wait when no request has started since (axafterwait --text: 561 to 56 ms). A snapshot that did wait says so (waited 412 ms for network quiet).set network-quiet <ms>sets the quiet window per session (default 500 ms; 250 ms reads a list that arrives in two fetches 400 ms apart too early), andwait --idleuses it.
Fixed
dragto a target that has to scroll into view drags the source. The target used to be scrolled into view before the press, which moved the source away: the press landed on whatever had scrolled under it (often the target itself), so the wrong element was dragged or no drag started. As in Playwright, the source is now pressed and the drag started first, and the target is scrolled into view while it is held.- A snapshot cut by
--limitthat was read in one call said “1 more lines” whatever its length; it now says “more lines”, as the lazy read does. start --profileopens one window, not two: the profile browser starts with--no-startup-window, as navigera’s own launches do, so there is no blank startup window and no “unsupported command-line flag” bar.
0.6.4 - 2026-10-31
Fixes found by running a real app’s end-to-end tests on navigera: snapshots no longer stall on pages that poll, drag and drop runs a real browser drag, clicks wait for a moving element to settle, and headless Chrome reports a mouse, as Playwright’s does.
Added
ax,screenshot,find,check <question>and--findtake--no-waitto skip the wait for fetched data; their help now describes the wait.fill --forceskips the new field checks (below).
Changed
-
The wait for fetched data before a snapshot ignores polls: a fetch/XHR to a URL (query string aside) already requested twice or more at a steady interval no longer counts as pending.
axon a page polling every 300 ms: 3011 ms -> 2 ms; a list that arrives from one held fetch is still waited for. Requests are forgotten when a new document starts. -
dragruns a real browser drag when the page starts one (Input.setInterceptDrags, thendragEnter/dragOveralong the path and a drop at the target), so HTML5 drag-and-drop lists and boards accept it. Without a drag the held mouse is released at the target as before. The result gains"path": "drag","mouse"or"synthetic". An engine that never reports a drag (Lightpanda) gives adraggablesource the page-side drag. -
clickanddblclickwait until the target’s box is the same in two reads with a frame drawn between them before pressing, so an element that is still moving without a CSS animation (a deferred commit, a script-driven slide) is clicked where it settles, not where it started.--forceskips the wait; a target that never stops is clicked when the settle budget ends. -
fillandfill --keyswait, within the command timeout, until the field is visible, enabled and editable, and the timeout error names the check that failed. Before, a hidden or disabled field was filled without a word.--forcekeeps the old behaviour. -
checkre-reads the state every 50 ms until it matches or the element budget runs out, so a controlled input that commits its state after the click returns (ReactonChange) is no longer reported as a failed click. -
Waits end at the page’s change instead of a frame later: on a page that keeps changing, frames lag; on a page writing to the DOM every 4 ms, a wait for
enabled, a URL or text returns 3-7 ms sooner (median). Wakes are at least 8 ms apart, so a page that changes constantly is not re-checked on every write. -
Headless launches report a mouse (
hover: hover,pointer: fine), as Playwright’s do. Before, an app that turns its mouse handlers on only for a hover-capable pointer treated headless Chrome as a touch screen. A touch device fromset devicestill reports touch. -
axasks Chrome for less layout data for its clickable-element marks: 2,000 rows with--limit 50: 618 -> 378 ms. The output is unchanged. -
wait --frameis woken by the frame’s changes too, as the other waits are, not only looked at every 100 ms. An element added to a same-origin frame 100-400 ms after the wait starts: seen 4 ms after it appears, not 65 (--selector, median;--text42 -> 4). A frame of another origin is out of the observer’s reach and is still looked at every 100 ms. -
tab-closecloses a tab the way a user does, withPage.closeon the page’s own session, so the page runs its unload path (beforeunload,pagehide) and a tab that hands work to another tab from it is heard at once.Target.closeTargetskippedbeforeunload. It stays as the fallback whenPage.closeis refused or the tab is still there after 1.5 s.
Fixed
set device Desktopandset device Laptopfailed after setting the viewport on Chrome 155, which refuses to turn touch off with a touch-point count of 0. A desktop device now turns touch off asset device offdoes.
0.6.3 - 2026-10-30
Refs on what a page made clickable: a <div> row with a click handler, a card
with cursor: pointer, an icon <span>, which agents had no ref for and dug
through the DOM with eval to reach. Waits woken by the page’s changes
instead of a timer, ax and the locators linear on long pages, and errors
that say what happened and what to do next.
Security
- A file navigera writes where you name it (
network --body-file,--har,screenshot,pdf,state save,install-skill) no longer follows a link already at that path: a link planted there (in a cloned project, or in a shared directory) is replaced by the file instead of having its target overwritten.
Added
axgives a ref to what a user could click that the accessibility tree doesn’t call actionable: elements with a click, mousedown or mouseup handler of their own and the outermost element of acursor: pointerarea. A<div>row or card, or an icon<span>, prints asclickable [ref=N]: <text>; an actionable node with no name or text shows its#idor.class, so two blank buttons can be told apart. A list or table listening for its items’ clicks gives each item or row the ref (one ref on the list would click whichever item is in the middle). Only what a user’s click can land on and tell apart is marked: not an empty box,pointer-events: none, something fully transparent (an ad’s click guard),visibility: hidden(a control shown on hover), underaria-hiddenorinert, inside another site’s frame, inside a link or button (includingrole=buttonandrole=link; they have the ref and get the click), or with no name, text,#idor.class(a hidden menu’s 287 blank tiles on one shop’s home page). A label whose control is hidden is what a user clicks, so it gets the ref. OneDOMSnapshotcall, Chrome only.- The skill says what a
clickableline is and to click it by ref before reaching foreval. evalruns statements too, withawaitandreturnat the top level (const r = await fetch('/api/cart'); r.status, or a body that ends inreturn): what was one expression or an arrow function before; a probe written either way works the first time.
Changed
-
--helpandhelp <command>fit 80 columns, list-s,--wait-msand--version, and showcheckonce with both of its forms. A usage error prints its usage once; an unknown command is matched only when it is close (goot→goto, notbogus→focus). -
Errors say what happened and what to do next, in plain words:
- a failed
gotosays what Chrome’s code means (the host name did not resolve, nothing answered on that port, the server answered 404 with no page and the tab now shows Chrome’s error page; chrome-headless-shell shows a blank page there instead, andgotosucceeds) and keeps the code at the end; - a click on a word that is not a ref (
click abc) says it was read as CSS and that refs are the numbers inax;ref12now reads as ref 12; - a stale ref, a covered or hidden element, and a browser that went away are described without DevTools terms (no “remote object”, “CDP command”, “DOM events”);
waittimeouts name--timeout-ms,tab-selectsays which tab numbers there are,set viewportsays what was missing;- flags that can’t go together (
--textwith--nth, a ref with--selector,--nearwith no locator) and asetof an unknown setting are refused when the command is read, with exit 2 and before any browser;--nthwith--textsays how to count instead; - a missing session says to start it the way navigera was called
(
primavera navigera -s <name> startfrom inside primavera); - a selector that isn’t CSS at all (
35.75,Total:) is said at once, with--textto use instead, not as the browser’squerySelectorerror after the wait; - the op is named once (
click selector "abc": …, notclick selector "abc": click: …), and the(os error 2)suffix is gone.
- a failed
-
Every MCP tool parameter has a description.
-
help <command>explains each flag in a line and shows examples that run as shown (each was run against a public practice site).help statusnames each field of whatstatusprints, andhelp axsays a target (a ref or any locator) scopes the tree to that element. -
Waits see a change within a frame of it. The
waitcommand checked every 100 ms and the element waits underclick,fill,getand the locators every 50 ms (100 for--text), so they answered 25-50 ms late on average; the page’s changes now wake them (a MutationObserver), and they still look at the old interval for what no observer sees (a stylesheet rule starting to match, a hover, a background tab). An element added 100-400 ms after the wait starts: seen 6 ms after it appears, not 58 (wait --selector, median), 5 not 34 (--role --name). -
clickwaits for an element that is there but not shown yet, as long as the element wait, as it already did for one under a closing overlay or disabled: a dialog’s button shown by a class after a second, a control made visible (visibility) or clickable (pointer-events), one sliding in from off-screen. It failed after 300 ms with “it has no box on screen”. An input hidden on purpose behind its label or card (a styled radio) still fails at once, naming what to click instead.
Fixed
clickon a table row whose middle falls between two cells clicks a cell of the row, instead of refusing the row as hidden behind its table.axread the page’s whole accessibility tree to print at most its limit, and Chrome’s full-tree call grows faster than the page: 8000 list items took 13 s and 32000 timed out. The tree is now fetched from the root down, children on demand, and the walk stops at the limit (2000 items 1.24 s -> 0.78 s, 8000 13.0 s -> 2.3 s, 32000 6.8 s, on Chromium 141). Output under the limit is byte-identical; over it, the last line says more lines are not shown, without a count the walk no longer learns.--limit 0,--format jsonand--max-depthread the whole tree as before, and a browser without the calls falls back to it.- A locator by role and name (
--role button --name …), by label, or by text read every candidate’sel.labels, which scans the whole document on its first read for each element: quadratic. On a page of 8000 buttons the first such call took 4.4 s (12000: about 9 s); it now takes under 50 ms, from one pass over the page’s labels. wait --text … --wait-ms <ms>waits that long;--wait-mswas accepted and ignored onwait, which then waited its default 5 s.click --forceon a covered element said the element had “no visible box”; the result now says the click was forced.- A bare target with
--nth,--nearor--frame(click .row --nth 2) was refused as an unexpected argument; it is the target. set offlinefailed on Chrome before the rule-based network calls (Chromium 141:'Network.emulateNetworkConditionsByRule' wasn't found). Such a browser now getsNetwork.emulateNetworkConditions, which also tells the page it is offline; newer browsers are unchanged.
0.6.2 - 2026-10-26
A security fix to 0.6.1’s MCP file limits: a link inside the working
directory no longer leads a file out of it. The npm platform packages move
to the @navigera scope; npm install navigera is unchanged.
Changed
- npm: the platform packages are
@navigera/<os>-<cpu>(@navigera/linux-x64and so on); npm refused the unscoped names. Install as before:npm install -g navigeraornpx navigera.
Security
- MCP: a file a
browser_runstep names could still land outside the working directory through a link inside it:link/../x, withlinkpointing elsewhere, was checked asxin the working directory and written beside the link’s target (state save,pdf,downloads --dir; 0.6.1). Each part of the path is now followed to where it leads before the next.., as the system does. Found by red-teaming the 0.6.1 fix.
0.6.1 - 2026-10-26
A security fix for the MCP server: every file a browser_run step names
stays inside the working directory. Also install-browser, which fetches
Chrome for Testing into navigera’s own cache; locate and history, which
primavera from-session builds tests from; and numbers from the page that
keep their exact value.
Added
-
install-browser [chrome-headless-shell | chrome] [--channel stable|beta|dev|canary]: downloads Chrome from Google’s Chrome for Testing into navigera’s own cache (--whereprints it;$NAVIGERA_BROWSERSmoves it), each file checked against the archive’s checksum. Headless sessions use an installed chrome-headless-shell first (about 3x faster to start); a Chrome installed this way is the fallback where the system has none. -
--near <text>with a locator: the match whose own part of the page (its card, row or section) shows the text.locatewrites it when a role and name match more than one element, before falling back to a CSS path. -
locate <target>: the locators that find an element again, most stable first (test id, role and name, label, placeholder, text, CSS), each kept only when the finder an op would use picks that element on the page;--frameadded inside a frame (a cross-origin one named from the page that holds it);secret: truefor a password field. An element in a shadow root gets its hosts’ path:--selector "#app >>> x-card >>> button". -
--selector "host >>> inner":innerinside the open shadow root of an elementhostmatches (or a shadow root under it), for every op,wait --selectorandget count. -
history [--clear]: the actions a session ran and its waits, oldest first; one on a ref carries its element’slocatefrom just before it ran, and each what the page said after it (title, heading, status messages). Whatprimavera from-sessionturns into a test.
Fixed
- Numbers from the page keep their exact value:
evaland the JSON output printed about one double in five slightly off in its last digit (9480.300552478155as9480.300552478157). Found by fuzzing. scroll --to bottom(the page’s, or inside a target) at the end already waits up to 2 s for a list that loads more to grow, then goes to the new end: two in a row, as a recorded session replays them, reached only the first page of a lazy list.historymarks a click that changed nothing (no_effect: true), so a test writer can drop it when the same click follows.- A
gototo a server that takes the connection and never answers fails at its timeout and cancels that navigation: the tab answers at once again (it had answered nothing while the dead load was pending, so the nexturlorax, and a test runner’s failure screenshot, each waited their whole timeout too: one failed step cost 105 s). - A control inside its
<label>(a select, a textarea) is named by the label’s own words for--label/--name, not with its options’ text. get boxof an element inside an iframe is in top-level viewport pixels (each frame’s offset added), asmousetakes them; it was relative to the element’s own frame.--naturalwith chrome-headless-shell as the browser (--chromium,$CHROME_BIN) is refused with a message instead of starting a browser that still tells pages it is headless.--fromcopies Chrome 156’s local storage, which is oneLocalStorageSQLite file there (with its journal) instead of aLocal Storagedirectory: sign-ins kept in local storage came over empty.- A
--profilebrowser falls back to no sandbox the way navigera’s own launches do (as root, withNAVIGERA_NO_SANDBOX, or after a first start that failed for want of one); it had exited and the profile could not be used on such a machine.
Changed
- Browser discovery no longer looks in other tools’ browser caches: it takes
--chromium,$CHROME_BIN, a system Chrome, Chromium or Edge, or whatinstall-browserinstalled. Point$CHROME_BINat a browser elsewhere.
Security
- tokio 1.54 (RUSTSEC-2026-0386).
- MCP:
browser_runsteps could read and write files outside the working directory (a route’sbody_fileserved any local file to the page;state save,network --har/--body-file,pdfanddownloads --dirwrote anywhere). Every file a step names is now held to the working directory, as uploads were, and the browser’s own pages (chrome://,devtools://) are refused;NAVIGERA_MCP_ALLOW_FILES=1lifts both. - The default download directory is now inside
navigera-<uid>in the temp directory (mode 0700), not a world-readablenavigera-downloads-<pid>.
0.6.0 - 2026-10-24
The browser handed over to a person and back, downloads, frames of any
origin and custom dropdowns; clicks that wait for what moves or covers
their element and say when they changed nothing; and reset and fork,
so a test runner gets a clean, separate session in about 40 ms.
Added
-
Element ops reach into same-origin iframes, framesets and
srcdocframes, and--selectorinto open shadow roots, when the page itself has no match (role/name and text locators already reached shadow roots).wait --selector/--text/--gone,get countandtextlook there too. -
--wait-ms <ms>(onstart,serve,mcpor one command;NAVIGERA_WAIT_MS): how long element ops wait for their element when they name no timeout. A test suite against a slow app sets it once. -
Downloads: files a page downloads are saved under
--download-dir(NAVIGERA_DOWNLOAD_DIR; default a directory of the session’s own) with the name the page gave them.downloadslists them;wait --download "*.csv"waits for one and returns its path.downloads --dir <path>gives one session a directory of its own (kept acrossreset; forks and other sessions keep theirs), so a test runner can give each test one and file names don’t carry over (report (2).csv) between tests. -
click --button right|middle(a native context-menu click),click --force(DOM events on the element, covered or not),select --add(add to a multiple select’s choice),tab-select <target id>,network --since <n>. -
selecton custom dropdowns (ARIA comboboxes such as react-select): it opens them the way a user does (ArrowDown, else a click), finds the option in the listbox that opened (portals included), types to filter when it isn’t listed, and clicks it. -
fill --keys: clear the field and type the value key by key, for forms whose app keeps only what was typed. -
scroll --to top|bottomwith a target scrolls inside it. -
Selectors, text and locators reach into cross-origin iframes (a payment provider’s card form, a sign-in frame) when the page itself has no match: navigera looks there from an isolated world of its own, which the page can’t see. A single
wait --text/--selectordoes too. With--attach, frames in another process stay ref-only. -
Library:
Driver::fork(andBrowserSession::fork), another driver on the same browser in a browser context of its own, movable to another thread: several sessions at once without a browser each (a test runner’s workers). Each takes only the tabs its own tabs open, and routes, credentials, the dialog answer and the downloads list are each session’s own. -
reset: a clean slate in the same browser, a new browser context (no cookies, storage or cache from before) with one blank tab, in about 40 ms instead of a new browser’s few hundred. For test runners that isolate scenarios, and agents that want to start over signed out.set, routes, init scripts and the dialog answer stay;reset --alldrops them too. -
--frame <css>on element targets andwait: look only inside that iframe or frame ("#outer >> #inner"for nested ones), of this origin or another, when two frames hold the same element (the first frame the CSS matches; when it matches none, the error says so). Without it, frames are searched as before. -
mouse move|down|up|click|wheel: raw mouse input at a point, for what has no element to aim at (a canvas, a map, a game): viewport CSS pixels, or from a target’s top-left corner (mouse click 30 40 --selector canvas). The pointer stays where it went and held buttons stay held, somouse down, moves (--stepsfor a smooth path),mouse updrags or draws;--count 2double-clicks;--button right|middle. A target out of view is scrolled into view first, as for a click;down,upandwheelwith a target go to its center; a click outside the viewport is refused instead of landing on nothing. MCP:browser_mouse. (An agent working through WebGames couldn’t play its canvas games without it.) -
--attach ws://host:port/devtools/page/<id>: work in that one tab of a running browser as it is, not in a window of its own (a tab a paused test run left open for a look).BrowserSession::page_endpointgives a tab’s. -
A click that changes nothing says so (
no_effectin its result): no DOM change, focus, form value, URL, scroll, clipboard write, request, dialog, tab or download within 200 ms. Requests that are the page reporting the click (beacons, analytics services, first-party/event/…-style paths whose answer changes nothing) don’t count, and the note names them; nor does what the browser or page loads on its own meanwhile (the tab’s icon, lazy images, fonts). Typically a button the page hasn’t wired up yet. A click that does something costs nothing extra; one whose effect comes a moment later now returns once it has. -
start --profile <name> --from chrome: the agent’s own browser, seeded with a copy of one of your Chrome profiles’ sign-ins – cookies (HttpOnly too), Local Storage, IndexedDB, saved logins – so it starts signed in without touching your running browser, on any Chrome version (attaching needs 144+).--fromtakeschrome,beta,dev,canary,chromium,edgeor a user data directory;--from-profilepicks a profile by directory, name or email. The copy replaces only a profile navigera made, and is refused while that profile’s browser runs. -
show/hide(MCP:browser_handoff): the session’s browser on the person’s screen with the same tabs and sign-ins, for a step only they can do – a login, a 2FA prompt, a CAPTCHA – and back off it. A--profilebrowser restarts on its own profile; one navigera launched hands its cookies and storage to its replacement. -
navigera profiles: the browsers and profiles a session can be handed (name, signed-in email, whether--attachreaches the browser now), and navigera’s own--profilebrowsers. -
--natural(NAVIGERA_NATURAL=1): a headless browser that shows pages no sign of being headless – the user agent the same Chrome sends with a window (page, workers, request headers) and a real screen size – checked in CI against a page that runs the checks anti-bot scripts make.
Changed
-
A click that may navigate waits for this tab’s navigation only when one can come: not for a
downloadlink, a link or form that opens another tab or window, or a link or submit inside a frame (which navigates the frame), and the wait ends as soon as the click opened a dialog, a tab or a download instead. Each of those clicks had cost 150-200 ms (a download link 170 ms, now about 20). -
A click waits for its element to stop moving: CSS animations and transitions on it or an ancestor (a modal sliding in) finish first, since pages often wire a dialog’s buttons only once it has opened. Pages with nothing animating pay nothing for this.
-
A script sent to a page that a navigation then replaced no longer waits out the whole timeout when the old page never answers (seen on macOS): a second after the new page commits, the op looks again there.
-
--textprefers an exact match inside a cross-origin frame (the “Pay” button of a payment frame) over page text that merely contains it (a “Payment details” heading), and never picks an iframe by its title. -
--textmatches what an element shows as well as what names it: a button showing “Add to cart” whosearia-labelis “Add Carbon Gear to cart” is the match, not the product card around it (a click on the card did nothing;no_effectcaught it in the benchmark). -
A click on an element that stays covered by another fails and names the covering element, instead of sending DOM events to an element no user could click. The element is first tried at other scroll positions, which is what uncovers it from a sticky header;
filldoes the same before typing into a field under a panel. -
selectmatches option labels with whitespace collapsed (non-breaking spaces count as spaces). -
typewith a target fails when the target doesn’t take focus, instead of typing into whatever has it. -
A disabled control is waited for (a Submit that unlocks once the form is valid) and otherwise refused:
clickandfillno longer report success on a disabled button, or a disabled or read-only field. -
When a selector’s first match is out of a user’s reach (a decoy copy of an id parked off the page) and another match isn’t, the other one is used.
-
A click on an element a user can’t see where it is (zero size, clipped, the hidden input of a styled radio card) fails at once and names what stands for it, instead of waiting and then sending DOM events.
-
An element op whose page is replaced by a navigation while it looks for its element (a step that saves with a request and then reloads) looks again on the new page instead of failing with “Inspected target navigated or closed”.
-
“not found” errors name the wait asked for (5000 ms, not 4750), and page errors drop the redundant
Error:prefix. -
A
--profilebrowser starts withnavigator.webdriverfalse, as navigera’s own launches always have, andquit --close-browserreturns once the profile is free, so astartright after it finds no browser on its way out. -
With a decision model on, a plain
axof a tree over 16 KB (NAVIGERA_AX_PREVIEW_BYTES) shows its first 40 lines and how many more there are, with a note that--findandcheckact on the whole page without reading it and thatax --limit <n>reads it all. The note alone came after the whole tree, once the agent had read it: in the 0.5.0 eval the agent reached for--findorcheckunasked in 3 of 12 runs.axwith a limit, a depth,--allor a scope is unchanged, and so isaxwithout a decider.
Fixed
- A page that stops answering (a script holding its thread, a renderer hung under load) cost one full command timeout per command after it: a click’s press timed out, then its clean-up and checks each waited the whole 35 s again (105 s for one click on a loaded macOS runner). Now, once a command on a tab has gone unanswered, the next ones wait 3 s until the tab answers again: the click fails about 35 s in and says it timed out, and the next op fails in seconds rather than another 35.
- A session’s only tab closing (the user closed it in an
--attached browser, or the page closed itself) left every op failing with Chrome’s “Session with given id not found”, whilestatusandtab-liststill showed the tab. Now ops say the tab is closed and thattab-newopens another;statusand the tab intab-listcarryclosed: true; andtab-new(orreset) replaces the closed tab instead of listing it beside the new one. An op still waiting on a tab when it closes (anevalawaiting a promise, agotowaiting for a slow page) ends then and says so, instead of after its 35 s timeout; with other tabs open, the session goes on in one of them. --attach <port>and--attach http://host:portsent Chrome’s/json/versionrequest without the port inHost, got back a WebSocket URL without one, and failed to connect.gotoandreloadtry once more when Chrome answers “Not attached to an active page” (the tab’s target was swapped mid-navigation), instead of failing the step.gotono longer retries a navigation that ended inERR_ABORTEDbecause it was an answer: a URL that downloads a file was fetched four times (four copies on disk); now the file is saved once andgotosucceeds withdownload: <name>, the page staying where it was. A 204 or 205 answer fails at once and says so, instead of after three retries.gotoandreloadno longer wait out their whole timeout when the load event is lost on its way (seen as rare 35 s stalls on slow macOS machines): every 2 s they also look at the page itself, and return once its document is the new one and has loaded.
0.5.0 - 2026-10-22
Lightpanda 1.0 as a second engine, the inspection and state commands agents reach for in Playwright, and a decision model the agent is told how to use.
Added
- Lightpanda 1.0 (
--engine lightpanda, orNAVIGERA_ENGINE=lightpandafor every command of a shell): the same commands andaxtree as on Chrome, with the engine’s gaps covered (see Fixed). CI runs the end-to-end suite on Lightpanda 1.0.1 and the nightly. drag <from> <to>: HTML5 drag and drop for adraggablesource (the drag events with one DataTransfer, since headless Chrome will not start a native drag from synthetic input, and on Lightpanda too), a held mouse moved across for anything else; the result says whether the drop was accepted.state save|load <file>: every cookie and the open tabs’ localStorage to a file and back, in Playwright’sstorageStateformat, so a signed-in session can be reused, by navigera or Playwright.status: the session’s engine, browser executable, CDP endpoint and tab state.console,errors,network(one request in full withnetwork <n>, HAR with--har): what the page logged and fetched; an uncaught exception during a command is reported in itsnotes.cookies(list/set/delete/clear),storage(local/session),pdf.set viewport|device|media|geo|offline|headers|ua|locale|timezone|credentials: emulation for every tab, replayed on new tabs.route/unroute: fail or answer requests by URL glob.init-script: code that runs before the page’s own in every document.- Locators on every element command:
--role/--name,--label,--placeholder,--testid,--nth. get,is,dblclick,focus,check/uncheck <target>,keydown/keyup,insert-text,wait --idle.- MCP:
browser_console_messages,browser_network_requests,browser_resize. notesin responses: what the engine did differently from what was asked (e.g. a Lightpanda popup loaded in the current tab).- Agent eval: any OpenAI-compatible model as the agent (
--agent openai, e.g. DeepSeek-V4.1-Flash through SiliconFlow) on the same tasks, prompt, turn limit and timeout as Gemini CLI; the decision benchmark takes such a model as its baseline; an OpenRouter head-to-head of tool variants (openrouter-eval.yml); a WebArena-Verified pilot (bench/webarena,webarena-eval.yml).
Changed
- With a decision model on, the agent is told how to use it where it
decides what to do next:
startadds a note (act with--find, verify withcheck, readaxonly for text to report), a tree over 60 lines ends with a one-line note that acting needs no reading, and the skill’s main loop shows the decider form next to the ax form. Before,startsaid only"decider": "<model>"and the usage sat in a later skill section: in eval run 39437548538 the agent saw the decider in 8 of 10 runs and used it in none; in run 39561380436 it used it in 3 of 15 unprompted. - Agent eval: a
preferencestask (a 170-line settings form, four named changes, a confirmation to check; the rest must stay as it was), the task the decider is measured on, and the three-arm protocol for measuring it (navigera,navigera+jev,navigera+jev-told, one model key each). starton a running session with a different engine fails instead of silently reusing it.
Fixed
reloadandgototry again after a moment when Chrome answers “Not attached to an active page” (the tab between two documents; nothing was done yet): seen once in a parallel test run.tab-selectreads 4 or more hex characters as a target id even when they are all digits.- A fork’s
tab-newopened its tab in the default browser context, with the owner’s cookies and storage; it opens in the fork’s own now. A download started by a subframe is listed for the session whose tab holds it (it went to the default list);reset --allno longer reports a dialog answered before it; a closed fork’s routes, answers and downloads are let go. - Sessions on one browser (
Driver::fork) no longer share routes, HTTP credentials, the dialog answer or the download list: each is the session’s own, keyed by its browser context. One worker’sroutemocked the others’ requests, andunrouteremoved theirs. --attach <port>and--attach http://host:port: the DevTools URL asked for carries the port again (Chrome answered with a WebSocket URL without one, and the connection was refused).- Lightpanda 1.0.x:
gotowaits for the load event (it returned at commit and gave up after 500 ms); confirm()/prompt() get the dialog policy (they returned false/null while reported accepted);target=_blankpages are followed in the one tab; iframes and stylesheets load; table cells keep their text inax; shadow-DOM clicks no longer wait 3 s; noxvfb-runwrapper; telemetry off throughLIGHTPANDA_DISABLE_TELEMETRY. - Dialogs, popups and JS navigations reported just before a command’s response are no longer missed by the op that caused them.
- Lightpanda: a navigation a click handler starts (
location.href = ...) is waited for. The engine reports it only after the click’s reply, so the click returned the old page withloading: true; it now returns the page the click led to. - Lightpanda:
axshows what open shadow roots hold. The engine’s tree stops at a shadow root and gives the nodes inside no ids, so a coupon field in a web component was invisible to an agent reading the snapshot; it is now listed under the root, without refs, with--textas the way to target it. - Lightpanda: a settings form reads in
axas it does on Chrome. A checkbox showed its default value"on", which reads like a state (in the DeepSeek eval, run 39530836547, both Lightpanda runs of the preferences task left a box unticked and failed); a<select>inside a<label>was named with every option (“Currency USD EUR GBP …”) next to a copy of the label; an unnamed table cell took two lines per checkbox. The preferences page is 177 lines on Lightpanda now, 243 before, 169 on Chrome. - Lightpanda: a checkbox or radio shows in
axthe state a click left it in. The engine’s tree keeps thecheckedattribute the page loaded with, so a box an agent had ticked read as unticked; in the DeepSeek eval (run 39691526622) an agent clicked one back that way and ran out of turns. - Lightpanda: an empty password field no longer reads as filled in
ax. The engine gives every password field the value*****; the snapshot now asks the field and shows a dot per character, or nothing, as on Chrome.
0.4.1 - 2026-10-14
Added
find <description>,check <question>and--find <description>on element commands: element choice and yes/no questions about the page, answered by a System One decision model (TypeSafe Jev) whenTYPESAFE_API_KEYorNAVIGERA_DECIDER_URLis set. MCP gainsbrowser_find,browser_checkand afindtarget in that case only. Typed values are redacted from what is sent.startreports"decider"with the model id, so an agent can tell without reading the key.server.jsonfor the MCP Registry (io.github.andrey-usa/navigera; the npm package carries the matchingmcpName), published by the release workflow whenPUBLISH_MCP_REGISTRYis set.install-skill --continue(.continue/skills);--globalfinds the home directory on Windows too.navigera mcp: a Model Context Protocol server on stdio for IDE agents (Cline, Roo Code, Kilo Code, Continue, VS Code, Claude Desktop, Cursor). 18 tools named after Playwright MCP’s,snapshot: trueon every action,browser_runfor several steps in one call; the browser starts on the first tool call.
Security
- Chrome’s renderer sandbox is on by default.
--no-sandboxis added only as root, withNAVIGERA_NO_SANDBOX=1, or when Chrome reports that the machine has no usable sandbox (with a warning on stderr). That last case is remembered per browser binary until it changes or the machine reboots, so only the first launch pays for the failed attempt (about 350 ms with Chrome for Testing on a GitHub runner). - Named sessions live in a private per-user directory
(
$XDG_RUNTIME_DIR/navigeraor$TMPDIR/navigera-<uid>, mode 0700) instead of/tmp/navigera-<name>.sock, so another user can’t squat a session’s socket; the log is 0600 and doesn’t follow symlinks. install.shandinstall.ps1verify the release’s SHA-256 checksum.- The skill tells agents to treat page text as data, not instructions.
[0.4.0] - 2026-10-09
First public release.
Added
- Named sessions: a warm browser behind a local socket, one shell command per
step (
navigera -s <name> start, thengoto,ax,click, …,quit). ax: the page as a compact accessibility tree with[ref=N]handles, including iframes (cross-origin too) and open shadow DOM; links carry theirurl.- Element commands by ref, CSS selector or visible text, with auto-wait; trusted mouse and keyboard input; tabs, dialogs, popups, uploads, screenshots.
serve: a JSON-lines protocol on stdin/stdout; a line holding a JSON array runs as a batch.eval --file <path>andeval -(script from stdin).--attach(your running Chrome 144+) and--profile(a persistent navigera browser).- An agent skill compiled into the binary (
navigera skill,navigera install-skill). - Prebuilt binaries for Linux (x86_64, aarch64; static), macOS (arm64, x86_64) and Windows (x64); install scripts for sh and PowerShell; packages for crates.io, npm, PyPI, Homebrew and Scoop.
Made from CHANGELOG.md.