{"slug": "hwatu-a-daemon-based-webkit-browser-with-13ms-window-spawns", "title": "Hwatu: A daemon-based WebKit browser with 13ms window spawns", "summary": "Hwatu, a daemon-based WebKit browser for tiling window managers, achieves a median window spawn time of 13 milliseconds (p90 35 ms) on a warm daemon by splitting the browser engine from the window, according to developer hongnoul. The open-source tool uses WebKitGTK 6, treats each window as a tab managed by the tiling WM, and includes a built-in content blocker and an automation protocol for AI coding agents.", "body_md": "A daemon-based web browser for tiling window managers. Real WebKit rendering, terminal-emulator spawn times.\n\nhwatu treats the browser window as a resource that other programs manage.\nFor humans, that program is your tiling WM. For coding agents, it's the\nagent harness: see [docs/agents.md](/hongnoul/hwatu/blob/main/docs/agents.md).\n\n```\ncurl -fsSL https://raw.githubusercontent.com/hongnoul/hwatu/main/scripts/install.sh | bash\n```\n\nRequires `webkitgtk-6.0`\n\nat runtime (the installer checks and tells you the\npackage for your distro). Or build from source:\n\n```\ncargo build --release   # needs rust + webkitgtk-6.0 dev headers\n```\n\nBrowsers conflate two things: the engine (slow to start, RAM-hungry) and the\nwindow (what you actually ask for). hwatu splits them, the same way\n`emacsclient`\n\n/`wezterm`\n\ndo:\n\nowns WebKitGTK 6, a prewarmed WebView pool, and all windows.`hwatud`\n\nis a thin client: one Unix-socket roundtrip to open a window.`hwatu`\n\nMeasured: **13 ms median** (p90 35 ms) from `hwatu <url>`\n\nto a mapped,\nloading window on a warm daemon; `--background`\n\nand `--headless`\n\nland at\n16 ms and 14 ms medians. The first-ever window pays a one-time engine/GPU\ninit (~200-400 ms). Full data and methodology: [docs/benchmarks.md](/hongnoul/hwatu/blob/main/docs/benchmarks.md).\n\nIf you want a minimal browser for a tiling window manager (Hyprland, sway, i3, river), the usual suspects trade differently:\n\n| hwatu | surf | qutebrowser | luakit | |\n|---|---|---|---|---|\n| Window spawn | 13 ms median (warm daemon) | full engine start per window | full engine start | full engine start |\n| Engine | WebKitGTK 6 | WebKitGTK 2 | QtWebEngine (Chromium) | WebKitGTK 2 |\n| Tabs | none, WM tiles are tabs | none | built-in | built-in |\n| Keyboard-driven UI | your WM's binds | patches | first-class vim binds | lua config |\n| Memory model | one shared engine, N views | one process per window | one big process | one process |\n\n**No tabs.** A tab is a window. Your tiling WM is the tab manager.**No chrome.** The WebView is the whole window.**Real rendering.** Full WebKit: JS, CSS, media, WebGL, as the frontend intended. No custom half-engine.**No ads.** Content blocking is built in and on by default, evaluated natively in WebKit's network process — zero JS, zero UI, zero spawn cost.\n\n```\nhwatu                      # open the launcher (autostarts hwatud)\nhwatu example.com          # open a URL (https:// implied)\nhwatu how to exit vim      # anything that isn't a URL is a web search\nhwatu --app-id mail url    # per-window app_id for WM window rules\nhwatu list                 # id, url, title of every window\nhwatu list --json          # same, as JSON (for wofi/rofi pipelines)\nhwatu close 2              # close window 2\nhwatu adblock              # content-blocker status (rule count, source)\nhwatu adblock off          # disable blocking (persisted; `on` re-enables)\nhwatu adblock update       # fetch EasyList + EasyPrivacy, recompile\nhwatu update               # self-update to the latest release\nhwatu quit                 # stop the daemon\n```\n\nThe daemon speaks a small automation protocol, built for AI coding\nagents (jcode has a native hwatu backend) and scripts that need to\nverify web UIs. A full verification pass (open headless, wait for\nload, eval, screenshot, close) measures **216 ms median**, ~75 ms\nwithout the screenshot ([docs/benchmarks.md](/hongnoul/hwatu/blob/main/docs/benchmarks.md)).\nFull guide: [docs/agents.md](/hongnoul/hwatu/blob/main/docs/agents.md).\n\n```\nhwatu --background localhost:3000           # open without stealing focus\nhwatu --headless localhost:3000             # open with no window at all\nhwatu snapshot                              # page text + clickable elements, as JSON\nhwatu click a --contains Pricing            # click (selector or --ref from snapshot)\nhwatu type 'input[name=q]' hello --enter    # fill an input, then submit\nhwatu console                               # console output, JS errors, failed requests\nhwatu eval 'document.title'                 # run JS in the page (async, JSON out)\nhwatu eval --id 2 'location.href'           # target a window by id\nhwatu goto localhost:3000                   # navigate + wait for the load\nhwatu goto --no-wait example.com            # navigate without waiting\nhwatu shot /tmp/page.png                    # screenshot the viewport (PNG)\nhwatu shot --full /tmp/page.png             # screenshot the whole document\nhwatu scroll h2 --contains Pricing          # scroll an element into view\nhwatu wait-load                             # block until the current load settles\nhwatu upload 'input[type=file]' ./pic.png   # set a file input's files from disk\nhwatu focus 2                               # raise/focus window 2 (materializes\n                                            # background/headless windows)\n```\n\n`eval`\n\naccepts a JavaScript *expression* (`document.title`\n\n) or a\n*function body* (`const n = ...; return n`\n\n); `await`\n\nworks in both\nand a returned Promise is awaited before the result comes back as\nJSON. Without `--id`\n\n, commands target the focused window, the window\nyour last automation command touched, or the only window. Everything\nis one JSON request over the Unix socket\n(`$XDG_RUNTIME_DIR/hwatu.sock`\n\n), so any language can drive it directly.\n\nA `--headless`\n\nwindow is a live session the WM never sees: an agent\ncan drive and screenshot it, and `hwatu focus <id>`\n\nlater materializes\nthat exact session as a normal window for the human to inspect.\n\nAgents get `--headless`\n\nwithout asking for it: when `hwatu`\n\nruns\ninside a coding-agent environment (detected by markers like\n`CLAUDECODE`\n\n, `JCODE_SOCKET`\n\n, `CURSOR_AGENT`\n\n), opens default to\nheadless so a verification flow never appears in the WM at all.\nHuman entries (shells, WM keybinds) keep the normal focused open, and\nan agent can pass `--focus`\n\nto deliberately show the user a window.\nThe agent default is configurable: set `HWATU_AGENT_MODE`\n\nor\n`\"agent_mode\": \"normal\" | \"background\" | \"headless\"`\n\nin\n`~/.config/hwatu/config.json`\n\n(e.g. `background`\n\nfor users who want\nverification windows visible-but-unfocused in their tiler).\nFor the background mode, because most tilers focus new windows\nregardless, the installer offers\n(default yes) to add a no-initial-focus rule for app-id\n`hwatu-background`\n\nto your niri/Hyprland/sway config; preseed\n`HWATU_WM_RULE=no`\n\nto skip.\n\n`Ctrl+w`\n\n(or `Ctrl+q`\n\n) closes the focused window. `Ctrl+l`\n\n(or `O`\n\n) opens the URL\nbar prefilled with the current address, `o`\n\nopens it blank; `Enter`\n\nnavigates, `Esc`\n\ncancels. `Ctrl+o`\n\n/ `Ctrl+i`\n\ngo back/forward in\nhistory (vim jumplist style). `Ctrl+r`\n\n/ `F5`\n\nreload the page.\n`Ctrl+Shift+j`\n\n/ `Ctrl+Shift+k`\n\nscroll\nthe page down/up by half a viewport. The daemon and engine stay warm.\n\nEvery bind is remappable in `~/.config/hwatu/keys.conf`\n\n, one\n`action = chord[, chord...]`\n\nper line (`none`\n\nunbinds):\n\n```\nback     = ctrl+o, alt+Left\nforward  = ctrl+i, alt+Right\nurl_edit = ctrl+l, O\nclose    = none\n```\n\nChords are `[ctrl+][alt+][shift+]key`\n\n; a key is a character (`o`\n\n, `/`\n\n)\nor a GDK key name (`slash`\n\n, `Left`\n\n, `Page_Down`\n\n). Uppercase implies\nshift. Actions: `close`\n\n, `url_open`\n\n, `url_edit`\n\n, `find`\n\n, `find_back`\n\n,\n`find_next`\n\n, `find_prev`\n\n, `scroll_down`\n\n, `scroll_up`\n\n, `back`\n\n,\n`forward`\n\n, `reload`\n\n. Chords with ctrl/alt always win over the page; bare keys\nreach the page first (an `o`\n\ntyped in a text box stays in the page).\n\nOn Wayland, `--app-id`\n\nnames the window for your compositor's rules:\n\n```\n# hyprland\nwindowrule = workspace 3, class:mail\n# sway\nassign [app_id=\"mail\"] workspace 3\n```\n\nUnfocused windows are suspended after `HWATU_DISCARD_SECS`\n\n(default\n120): navigation history is serialized to `~/.cache/hwatu/discard/`\n\n,\nthe web process is killed, and the RAM comes back. Focusing the window\nrestores it from the prewarm pool, so resume feels instant. If a page's\nweb process crashes or is OOM-killed, the bar offers\n`page crashed, reload? [y/n]`\n\ninstead of leaving a white window.\n\nIf the *daemon* dies uncleanly (crash, OOM kill, logout), the next\n`hwatud`\n\nreopens every window at its last URL: the open-window set is\nsnapshotted to `~/.local/state/hwatu/session.json`\n\nas you browse. A\nclean `hwatu quit`\n\nremoves the snapshot, so intentional exits stay\nexits.\n\nhwatu's one piece of chrome: a single-line vim-style bar at the bottom of the window, hidden until summoned. Everything interactive lives there, so the resting state stays chromeless.\n\n-\n**The launcher**: a bare`hwatu`\n\nopens a keybind cheat sheet with the URL bar already open — type a URL or search and hit Enter.`Esc`\n\ncloses an untouched launcher window, so a mis-fired keybind costs nothing. Set`HWATU_HOME`\n\nto get a home page instead. -\n**Open a URL**:`Ctrl+l`\n\n/`O`\n\nedit the current address,`o`\n\nstarts blank. Input is normalized like the CLI (`example.com`\n\ngets`https://`\n\n, loopback hosts get`http://`\n\n), and anything that doesn't look like a URL becomes a web search. -\n**Web search**: input with spaces or without a dot (`rust borrow checker`\n\n,`vim`\n\n) searches with your configured engine. The installer asks which one (DuckDuckGo, Google, Bing, Brave, Startpage, Kagi, Ecosia — default DuckDuckGo); change it any time in`~/.config/hwatu/search.conf`\n\n, either an engine name or a URL template like`https://example.com/search?q=%s`\n\n. Applies without a daemon restart. -\n**Find in page**:`/`\n\nopens forward search,`?`\n\nbackward. Matches highlight incrementally with a live count.`Enter`\n\ncommits (focus returns to the page,`n`\n\n/`N`\n\njump next/previous),`Esc`\n\ncancels. A`/`\n\ntyped into a page's text box still goes to the page. -\n**Permission prompts**: mic, camera, location, notifications, clipboard and friends appear as`example.com wants microphone [y/n]`\n\n. Decisions are remembered per site for the daemon's lifetime and apply across windows. Nothing is written to disk. -\n**TLS errors**: failed certificate loads show the reason (expired, unknown issuer, hostname mismatch, ...).`y`\n\nadds a session exception for that host and reloads;`n`\n\n/`Esc`\n\nleaves the load stopped. Exceptions reset when the daemon exits. -\n**Download status**: saved/failed notices flash briefly.\n\nNo dialogs, no download manager. Attachments and unrenderable MIME\ntypes save straight to `HWATU_DOWNLOAD_DIR`\n\n, or your xdg-user-dirs\ndownload folder, or `~/Downloads`\n\n. Name collisions get a ` (n)`\n\nsuffix. The bar flashes the destination when a download finishes.\n\nA baseline filter list is embedded in the binary, so blocking works\noffline on first run. `hwatu adblock update`\n\nupgrades to full EasyList +\nEasyPrivacy (117,431 compiled rules); compiled rulesets are cached, so\nonly the first start after a list change pays compile cost (~5 s), warm\nstarts load in ~0.3 s. Rules run in WebKit's content-extension engine in\nthe network process — the same machinery as Safari content blockers —\nso there is no JavaScript in the request path and no per-window cost.\n\n- Toggle:\n`hwatu adblock on|off`\n\napplies live to every open window and persists in`~/.config/hwatu/config.json`\n\n.`HWATU_ADBLOCK=off`\n\noverrides at daemon startup. - Own filters: put ABP-syntax rules in\n`~/.config/hwatu/filters.txt`\n\n; they are appended to whatever lists are active. - Filter kinds the engine cannot express declaratively ($csp, $redirect, scriptlets, procedural cosmetics) are skipped, never approximated, so a filter-list update can't break page loads.\n\nNo config file for engine knobs. They are set to their correct values\nin code (GPU compositing always on), and the only surfaces are\n`keys.conf`\n\n(above) and environment variables read by `hwatud`\n\n:\n\n`HWATU_HOME`\n\n– page opened by a bare`hwatu`\n\n(default: the built-in launcher, a keybind cheat sheet with the URL bar pre-opened) (default[https://hongnoul.github.io/hwatu/](https://hongnoul.github.io/hwatu/), use`about:blank`\n\nfor none).`HWATU_DISCARD_SECS`\n\n– seconds an unfocused window keeps its live WebView before being suspended to save RAM (default 120, 0 disables).`HWATU_DOWNLOAD_DIR`\n\n– where downloads land (default: xdg-user-dirs download folder, falling back to`~/Downloads`\n\n).`HWATU_WEBKIT_FEATURES=Ident:on,Other:off`\n\n– flip individual WebKit runtime features on odd hardware. Unknown identifiers are ignored.- Standard\n`WEBKIT_*`\n\n/`GSK_RENDERER`\n\nvars pass through untouched.\n\nScrolling smoothness scales with your distro's WebKitGTK: 2.46+ paints\nwith Skia on the GPU and is markedly smoother. `hwatud`\n\nlogs its\nWebKitGTK version, session type, and renderer at startup; include that\nline in any jank report.\n\n``` php\nhwatu <url>  --unix socket-->  hwatud (GTK main loop)\n                                 ├── prewarmed WebView (adopted instantly,\n                                 │   next one warmed in idle time)\n                                 ├── window registry (id -> WebView)\n                                 └── WebKit: shared network process,\n                                     per-site web processes\n```\n\nCrates:\n\n`crates/ipc`\n\n– newline-delimited JSON protocol (`Request`\n\n/`Response`\n\n)`crates/hwatud`\n\n– daemon: GTK4 + webkit6, socket server on the GLib loop`crates/hwatu`\n\n– client: no GTK linkage, connects or spawns the daemon\n\n- Background window suspension + discard-to-disk (RAM reclaim)\n- Per-window\n`app_id`\n\nfor WM window rules - URL bar (\n`Ctrl+l`\n\n,`o`\n\n,`O`\n\n) - Crash resilience: reopen windows after an unclean daemon death\n- Automation protocol: eval / goto / shot / wait-load / upload / scroll / focus\n-\n`--background`\n\n/`--headless`\n\nwindow modes (no focus steal, no window) - Measured benchmarks in docs (spawn, verification loop, memory)\n- Agent-ergonomic eval (expression or function body, probed server-side)\n- Sticky automation target: id-less commands follow the last-driven window\n-\n`scroll`\n\nwith selector/nth/contains disambiguation + landing report - Full-document screenshots (\n`shot --full`\n\n) - Persistent cookies (logins survive daemon restarts)\n- Text/a11y page snapshot (\n`hwatu snapshot`\n\n): token-cheap page state for agents - First-class interaction:\n`hwatu click`\n\n/`hwatu type`\n\n(selector or snapshot ref) - Console + network capture for verification loops (\n`hwatu console`\n\n) - Link hints\n- Profiles (separate cookie jars / web contexts); per-agent isolation\n- Displayless operation (nested headless compositor) for CI", "url": "https://wpnews.pro/news/hwatu-a-daemon-based-webkit-browser-with-13ms-window-spawns", "canonical_source": "https://github.com/hongnoul/hwatu", "published_at": "2026-07-22 16:42:25+00:00", "updated_at": "2026-07-22 16:52:28.125603+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence"], "entities": ["hwatu", "WebKitGTK 6", "hongnoul", "Hyprland", "sway", "i3", "river", "jcode"], "alternates": {"html": "https://wpnews.pro/news/hwatu-a-daemon-based-webkit-browser-with-13ms-window-spawns", "markdown": "https://wpnews.pro/news/hwatu-a-daemon-based-webkit-browser-with-13ms-window-spawns.md", "text": "https://wpnews.pro/news/hwatu-a-daemon-based-webkit-browser-with-13ms-window-spawns.txt", "jsonld": "https://wpnews.pro/news/hwatu-a-daemon-based-webkit-browser-with-13ms-window-spawns.jsonld"}}