<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: lipski</title>
    <description>The latest articles on DEV Community by lipski (@lipski).</description>
    <link>https://dev.to/lipski</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3894250%2F1168b5e2-a48e-4931-a6f1-0dcefe5a0c8b.jpg</url>
      <title>DEV Community: lipski</title>
      <link>https://dev.to/lipski</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lipski"/>
    <language>en</language>
    <item>
      <title>I built a browser daemon for AI coding agents because Playwright wasn't enough</title>
      <dc:creator>lipski</dc:creator>
      <pubDate>Thu, 23 Apr 2026 12:26:47 +0000</pubDate>
      <link>https://dev.to/lipski/i-built-a-browser-daemon-for-ai-coding-agents-because-playwright-wasnt-enough-jp0</link>
      <guid>https://dev.to/lipski/i-built-a-browser-daemon-for-ai-coding-agents-because-playwright-wasnt-enough-jp0</guid>
      <description>&lt;p&gt;&lt;strong&gt;The problem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I use Claude Code on Windows — Anthropic's coding-agent CLI that runs alongside my editor. It drives my codebase through a pipeline of exec-style tool calls.&lt;/p&gt;

&lt;p&gt;One day I wanted Claude to open a web UI, log in to my account on a SaaS dashboard, export a CSV, and diff it against yesterday's. Simple task. I reached for Playwright.&lt;/p&gt;

&lt;p&gt;It didn't work. Here's why.&lt;/p&gt;

&lt;p&gt;Claude Code's Bash tool can start a long-running process with a &lt;strong&gt;run_in_background&lt;/strong&gt; flag and read its stdout later. But it &lt;strong&gt;cannot write to that process's stdin&lt;/strong&gt; after launch. So any stdin-based REPL — Playwright's Inspector, a Node &lt;strong&gt;readline&lt;/strong&gt; loop, anything interactive — dies the moment the first tool call returns. The browser session closes, cookies are gone, and the next Claude turn has to re-login.&lt;/p&gt;

&lt;p&gt;I needed a persistent browser. One login. Many turns. Live for days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The architecture&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The solution is obvious in hindsight: &lt;strong&gt;HTTP-RPC&lt;/strong&gt;. Instead of a stdin REPL, run the browser-holding daemon as a small HTTP server on &lt;strong&gt;127.0.0.1:&lt;/strong&gt;, and have each agent tool call fire a one-shot CLI client that POSTs a JSON command to it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgsloqui4spjuhmfomkw0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgsloqui4spjuhmfomkw0.png" alt=" " width="800" height="332"&gt;&lt;/a&gt;&lt;br&gt;
The daemon binds to an ephemeral port (&lt;strong&gt;server.listen(0)&lt;/strong&gt;), writes the chosen port into a lockfile at &lt;strong&gt;%LOCALAPPDATA%\NativeWright\daemon.json&lt;/strong&gt; (or the platform equivalent), and handles &lt;strong&gt;POST /cmd&lt;/strong&gt; requests with a minimal command table — &lt;strong&gt;goto, click, fill, type, shot&lt;/strong&gt;, etc.&lt;/p&gt;

&lt;p&gt;The CLI client reads the lockfile to find the port, posts the command as JSON, prints the response, and exits. Each agent tool call is a one-shot. The browser stays.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Patchright, not Playwright&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My target sites include Google (Gemini), LinkedIn, ChatGPT — anything with non-trivial anti-bot detection. Vanilla Playwright gets flagged immediately: &lt;strong&gt;navigator.webdriver === true&lt;/strong&gt;, weird &lt;strong&gt;Runtime.enable&lt;/strong&gt; CDP events leaking into the page, obvious CLI flags like &lt;strong&gt;--enable-automation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Patchright is a drop-in Playwright fork with the detection vectors patched out. Chromium-only, maintained by the community. Same API as Playwright. I just require it instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;patchright&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launchPersistentContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userDataDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chrome&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headless&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;viewport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;acceptDownloads&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire browser setup. One call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why a human-behavior layer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Stealth-patched browser is half the story. The other half is &lt;strong&gt;input&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Modern anti-bot systems don't just fingerprint the browser — they fingerprint the &lt;strong&gt;input stream&lt;/strong&gt;. Mouse events at exact pixel coordinates with zero jitter. Keystrokes at uniform 50ms intervals. Scroll events with identical deltas. Log-in forms filled in 5 ms flat. These are all easy tells.&lt;/p&gt;

&lt;p&gt;A sophisticated stealth-patched browser with robotic input is worse than a vanilla Playwright with patient human input. The input layer has to match the browser layer.&lt;/p&gt;

&lt;p&gt;So I wrote &lt;strong&gt;src/human.js&lt;/strong&gt;. It's ~500 lines, has zero dependencies beyond Node built-ins, and runs entirely on Patchright's existing &lt;strong&gt;page.mouse&lt;/strong&gt;.* and &lt;strong&gt;page.keyboard&lt;/strong&gt;.* primitives. Every interaction command in NativeWright — &lt;strong&gt;click, fill, type, hover, press, scroll&lt;/strong&gt; — routes through it by default. Opt out per-call with &lt;strong&gt;--raw=true&lt;/strong&gt; when you need robotic precision (invisible elements, programmatic fills, scripted logins where slow typing would itself look suspicious).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here's what it covers:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mouse paths&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Real human mouse movement is &lt;strong&gt;curved&lt;/strong&gt; (your wrist rotates, your arm has momentum) and &lt;strong&gt;non-uniform in velocity&lt;/strong&gt; (slow start, fast middle, slow precise arrival). NativeWright generates cubic Bézier paths between two points:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;samplePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;p1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p2&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildControlPoints&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;easing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pickEasing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;easing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bezier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;pt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;gaussian&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;jitter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;pt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;gaussian&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;jitter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each point gets per-sample Gaussian jitter. Three different easing curves are picked randomly per move (ease-in-out, ease-out, near-linear) so detectors can't fingerprint a single velocity profile. Longer moves have a ~20% chance of &lt;strong&gt;overshoot-and-correct&lt;/strong&gt; (fling past, pause, snap back) — what real humans do with a mouse they're flinging across the screen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fitts-law timing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The total duration of a mouse move scales with distance and target width per Fitts's law:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;duration ≈ 80 + 90 * log2(distance / width + 1)  ms  (log-normal-jittered)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Tiny distant targets take longer. Big close targets are fast. Same asymmetry real users have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keystroke cadence&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Typing is log-normal, not uniform. Per-character base mean varies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;charBaseMs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;140&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;.,!?;:&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;170&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;0-9&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;130&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;A-Z&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Shift hold&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;a-z&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;140&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Post-space and post-punctuation pauses are extended (cognitive break). Occasional mid-sentence hesitations of 700-2200ms simulate thought. Per-key down-up dwell times are independently log-normal.&lt;/p&gt;

&lt;p&gt;Bonus: a typo simulator inserts the wrong neighboring QWERTY key ~0.8% of the time, pauses (the "noticing" beat), presses Backspace, corrects. &lt;strong&gt;Auto-disabled for password/OTP/secret/CVV fields&lt;/strong&gt; via attribute sniffing — no one wants a typo in their API token.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scroll physics&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;page.mouse.wheel()&lt;/strong&gt; with variable tick magnitude (60-180 px), log-normal inter-tick gaps, occasional longer "reading" pauses, and &lt;strong&gt;edge-detection&lt;/strong&gt;: polls &lt;strong&gt;window.scrollY&lt;/strong&gt; and stops after two consecutive ticks without progress. No infinite-wheeling on short pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tradeoffs I accepted&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Stealth-patched Chrome runs with &lt;strong&gt;--disable-blink-features=AutomationControlled&lt;/strong&gt; to hide &lt;strong&gt;navigator.webdriver&lt;/strong&gt;. Newer Chrome versions show a yellow "unsupported flag" warning bar when this flag is set. I tried stripping the flag — webdriver immediately becomes visible. That flag is the entire stealth. The warning is visible only to the human looking at the Chrome window; pages inside can't read it.&lt;/p&gt;

&lt;p&gt;Patchright also includes &lt;strong&gt;--no-sandbox&lt;/strong&gt; in its default args by default. That's a bot-telltale AND it triggers its own yellow warning bar. I strip it by default on Windows / macOS / non-root Linux (where the kernel sandbox works fine without it). Kept on root Linux (Docker, CI) where Chromium refuses to boot without it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How agents actually use it&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After installing (&lt;strong&gt;npm install -g nativewright &amp;amp;&amp;amp; npx patchright install chrome&lt;/strong&gt;), an agent workflow looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check if daemon running&lt;/span&gt;
nativewright status

&lt;span class="c"&gt;# Start (in background)&lt;/span&gt;
nativewright start &amp;amp;
nativewright wait-ready &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;30000

&lt;span class="c"&gt;# Drive&lt;/span&gt;
nativewright goto https://gemini.google.com/app
nativewright &lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="s2"&gt;".ql-editor.textarea"&lt;/span&gt; &lt;span class="s2"&gt;"generate a sunset photo"&lt;/span&gt;
nativewright press Enter
nativewright wait-for &lt;span class="s2"&gt;"img[alt*=generated i]"&lt;/span&gt;
nativewright click &lt;span class="s2"&gt;"button[aria-label='Download full size image']"&lt;/span&gt;

&lt;span class="c"&gt;# Stop when done (flush cookies to disk)&lt;/span&gt;
nativewright stop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The daemon preserves state between every command. The human-behavior layer kicks in for type, click, wait-for. Next agent turn an hour later — same profile, same login, same cookies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's in the package&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Node.js daemon + CLI + REPL (one binary, three modes)&lt;/li&gt;
&lt;li&gt;30 browser-automation commands&lt;/li&gt;
&lt;li&gt;Two Claude Code skills pre-packaged in claude-skills/&lt;/li&gt;
&lt;li&gt;Cross-platform: Windows, macOS, Linux&lt;/li&gt;
&lt;li&gt;Zero runtime deps beyond patchright&lt;/li&gt;
&lt;li&gt;CI-tested on all three OSes&lt;/li&gt;
&lt;li&gt;47 KB npm package, 149 KB unpacked&lt;/li&gt;
&lt;li&gt;Apache-2.0&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxbttjtj309arcvka56lr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxbttjtj309arcvka56lr.png" alt=" " width="800" height="616"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Links&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/lipski-lite/nativewright" rel="noopener noreferrer"&gt;https://github.com/lipski-lite/nativewright&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/nativewright" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/nativewright&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of this sounds useful, a ⭐ on the repo means a lot. PRs welcome.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>ai</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
