cd /news/developer-tools/hwatu-a-daemon-based-webkit-browser-… · home topics developer-tools article
[ARTICLE · art-68892] src=github.com ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Hwatu: A daemon-based WebKit browser with 13ms window spawns

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.

read10 min views1 publishedJul 22, 2026
Hwatu: A daemon-based WebKit browser with 13ms window spawns
Image: source

A daemon-based web browser for tiling window managers. Real WebKit rendering, terminal-emulator spawn times.

hwatu treats the browser window as a resource that other programs manage. For humans, that program is your tiling WM. For coding agents, it's the agent harness: see docs/agents.md.

curl -fsSL https://raw.githubusercontent.com/hongnoul/hwatu/main/scripts/install.sh | bash

Requires webkitgtk-6.0

at runtime (the installer checks and tells you the package for your distro). Or build from source:

cargo build --release   # needs rust + webkitgtk-6.0 dev headers

Browsers conflate two things: the engine (slow to start, RAM-hungry) and the window (what you actually ask for). hwatu splits them, the same way emacsclient

/wezterm

do:

owns WebKitGTK 6, a prewarmed WebView pool, and all windows.hwatud

is a thin client: one Unix-socket roundtrip to open a window.hwatu

Measured: 13 ms median (p90 35 ms) from hwatu <url>

to a mapped, window on a warm daemon; --background

and --headless

land at 16 ms and 14 ms medians. The first-ever window pays a one-time engine/GPU init (~200-400 ms). Full data and methodology: docs/benchmarks.md.

If you want a minimal browser for a tiling window manager (Hyprland, sway, i3, river), the usual suspects trade differently:

hwatu surf qutebrowser luakit
Window spawn 13 ms median (warm daemon) full engine start per window full engine start full engine start
Engine WebKitGTK 6 WebKitGTK 2 QtWebEngine (Chromium) WebKitGTK 2
Tabs none, WM tiles are tabs none built-in built-in
Keyboard-driven UI your WM's binds patches first-class vim binds lua config
Memory model one shared engine, N views one process per window one big process one process

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.

hwatu                      # open the launcher (autostarts hwatud)
hwatu example.com          # open a URL (https:// implied)
hwatu how to exit vim      # anything that isn't a URL is a web search
hwatu --app-id mail url    # per-window app_id for WM window rules
hwatu list                 # id, url, title of every window
hwatu list --json          # same, as JSON (for wofi/rofi pipelines)
hwatu close 2              # close window 2
hwatu adblock              # content-blocker status (rule count, source)
hwatu adblock off          # disable blocking (persisted; `on` re-enables)
hwatu adblock update       # fetch EasyList + EasyPrivacy, recompile
hwatu update               # self-update to the latest release
hwatu quit                 # stop the daemon

The daemon speaks a small automation protocol, built for AI coding agents (jcode has a native hwatu backend) and scripts that need to verify web UIs. A full verification pass (open headless, wait for load, eval, screenshot, close) measures 216 ms median, ~75 ms without the screenshot (docs/benchmarks.md). Full guide: docs/agents.md.

hwatu --background localhost:3000           # open without stealing focus
hwatu --headless localhost:3000             # open with no window at all
hwatu snapshot                              # page text + clickable elements, as JSON
hwatu click a --contains Pricing            # click (selector or --ref from snapshot)
hwatu type 'input[name=q]' hello --enter    # fill an input, then submit
hwatu console                               # console output, JS errors, failed requests
hwatu eval 'document.title'                 # run JS in the page (async, JSON out)
hwatu eval --id 2 'location.href'           # target a window by id
hwatu goto localhost:3000                   # navigate + wait for the load
hwatu goto --no-wait example.com            # navigate without waiting
hwatu shot /tmp/page.png                    # screenshot the viewport (PNG)
hwatu shot --full /tmp/page.png             # screenshot the whole document
hwatu scroll h2 --contains Pricing          # scroll an element into view
hwatu wait-load                             # block until the current load settles
hwatu upload 'input[type=file]' ./pic.png   # set a file input's files from disk
hwatu focus 2                               # raise/focus window 2 (materializes

eval

accepts a JavaScript expression (document.title

) or a function body (const n = ...; return n

); await

works in both and a returned Promise is awaited before the result comes back as JSON. Without --id

, commands target the focused window, the window your last automation command touched, or the only window. Everything is one JSON request over the Unix socket ($XDG_RUNTIME_DIR/hwatu.sock

), so any language can drive it directly.

A --headless

window is a live session the WM never sees: an agent can drive and screenshot it, and hwatu focus <id>

later materializes that exact session as a normal window for the human to inspect.

Agents get --headless

without asking for it: when hwatu

runs inside a coding-agent environment (detected by markers like CLAUDECODE

, JCODE_SOCKET

, CURSOR_AGENT

), opens default to headless so a verification flow never appears in the WM at all. Human entries (shells, WM keybinds) keep the normal focused open, and an agent can pass --focus

to deliberately show the user a window. The agent default is configurable: set HWATU_AGENT_MODE

or "agent_mode": "normal" | "background" | "headless"

in ~/.config/hwatu/config.json

(e.g. background

for users who want verification windows visible-but-unfocused in their tiler). For the background mode, because most tilers focus new windows regardless, the installer offers (default yes) to add a no-initial-focus rule for app-id hwatu-background

to your niri/Hyprland/sway config; preseed HWATU_WM_RULE=no

to skip.

Ctrl+w

(or Ctrl+q

) closes the focused window. Ctrl+l

(or O

) opens the URL bar prefilled with the current address, o

opens it blank; Enter

navigates, Esc

cancels. Ctrl+o

/ Ctrl+i

go back/forward in history (vim jumplist style). Ctrl+r

/ F5

reload the page. Ctrl+Shift+j

/ Ctrl+Shift+k

scroll the page down/up by half a viewport. The daemon and engine stay warm.

Every bind is remappable in ~/.config/hwatu/keys.conf

, one action = chord[, chord...]

per line (none

unbinds):

back     = ctrl+o, alt+Left
forward  = ctrl+i, alt+Right
url_edit = ctrl+l, O
close    = none

Chords are [ctrl+][alt+][shift+]key

; a key is a character (o

, /

) or a GDK key name (slash

, Left

, Page_Down

). Uppercase implies shift. Actions: close

, url_open

, url_edit

, find

, find_back

, find_next

, find_prev

, scroll_down

, scroll_up

, back

, forward

, reload

. Chords with ctrl/alt always win over the page; bare keys reach the page first (an o

typed in a text box stays in the page).

On Wayland, --app-id

names the window for your compositor's rules:

windowrule = workspace 3, class:mail
assign [app_id="mail"] workspace 3

Unfocused windows are suspended after HWATU_DISCARD_SECS

(default 120): navigation history is serialized to ~/.cache/hwatu/discard/

, the web process is killed, and the RAM comes back. Focusing the window restores it from the prewarm pool, so resume feels instant. If a page's web process crashes or is OOM-killed, the bar offers page crashed, reload? [y/n]

instead of leaving a white window.

If the daemon dies uncleanly (crash, OOM kill, logout), the next hwatud

reopens every window at its last URL: the open-window set is snapshotted to ~/.local/state/hwatu/session.json

as you browse. A clean hwatu quit

removes the snapshot, so intentional exits stay exits.

hwatu'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.

The launcher: a barehwatu

opens a keybind cheat sheet with the URL bar already open — type a URL or search and hit Enter.Esc

closes an untouched launcher window, so a mis-fired keybind costs nothing. SetHWATU_HOME

to get a home page instead. - Open a URL:Ctrl+l

/O

edit the current address,o

starts blank. Input is normalized like the CLI (example.com

getshttps://

, loopback hosts gethttp://

), and anything that doesn't look like a URL becomes a web search. - Web search: input with spaces or without a dot (rust borrow checker

,vim

) 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

, either an engine name or a URL template likehttps://example.com/search?q=%s

. Applies without a daemon restart. - Find in page:/

opens forward search,?

backward. Matches highlight incrementally with a live count.Enter

commits (focus returns to the page,n

/N

jump next/previous),Esc

cancels. A/

typed into a page's text box still goes to the page. - Permission prompts: mic, camera, location, notifications, clipboard and friends appear asexample.com wants microphone [y/n]

. Decisions are remembered per site for the daemon's lifetime and apply across windows. Nothing is written to disk. - TLS errors: failed certificate loads show the reason (expired, unknown issuer, hostname mismatch, ...).y

adds a session exception for that host and reloads;n

/Esc

leaves the load stopped. Exceptions reset when the daemon exits. - Download status: saved/failed notices flash briefly.

No dialogs, no download manager. Attachments and unrenderable MIME types save straight to HWATU_DOWNLOAD_DIR

, or your xdg-user-dirs download folder, or ~/Downloads

. Name collisions get a (n)

suffix. The bar flashes the destination when a download finishes.

A baseline filter list is embedded in the binary, so blocking works offline on first run. hwatu adblock update

upgrades to full EasyList + EasyPrivacy (117,431 compiled rules); compiled rulesets are cached, so only the first start after a list change pays compile cost (~5 s), warm starts load in ~0.3 s. Rules run in WebKit's content-extension engine in the network process — the same machinery as Safari content blockers — so there is no JavaScript in the request path and no per-window cost.

  • Toggle: hwatu adblock on|off

applies live to every open window and persists in~/.config/hwatu/config.json

.HWATU_ADBLOCK=off

overrides at daemon startup. - Own filters: put ABP-syntax rules in ~/.config/hwatu/filters.txt

; 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.

No config file for engine knobs. They are set to their correct values in code (GPU compositing always on), and the only surfaces are keys.conf

(above) and environment variables read by hwatud

:

HWATU_HOME

– page opened by a barehwatu

(default: the built-in launcher, a keybind cheat sheet with the URL bar pre-opened) (defaulthttps://hongnoul.github.io/hwatu/, useabout:blank

for none).HWATU_DISCARD_SECS

– seconds an unfocused window keeps its live WebView before being suspended to save RAM (default 120, 0 disables).HWATU_DOWNLOAD_DIR

– where downloads land (default: xdg-user-dirs download folder, falling back to~/Downloads

).HWATU_WEBKIT_FEATURES=Ident:on,Other:off

– flip individual WebKit runtime features on odd hardware. Unknown identifiers are ignored.- Standard WEBKIT_*

/GSK_RENDERER

vars pass through untouched.

Scrolling smoothness scales with your distro's WebKitGTK: 2.46+ paints with Skia on the GPU and is markedly smoother. hwatud

logs its WebKitGTK version, session type, and renderer at startup; include that line in any jank report.

hwatu <url>  --unix socket-->  hwatud (GTK main loop)
                                 ├── prewarmed WebView (adopted instantly,
                                 │   next one warmed in idle time)
                                 ├── window registry (id -> WebView)
                                 └── WebKit: shared network process,
                                     per-site web processes

Crates:

crates/ipc

– newline-delimited JSON protocol (Request

/Response

)crates/hwatud

– daemon: GTK4 + webkit6, socket server on the GLib loopcrates/hwatu

– client: no GTK linkage, connects or spawns the daemon

  • Background window suspension + discard-to-disk (RAM reclaim)
  • Per-window app_id

for WM window rules - URL bar ( Ctrl+l

,o

,O

) - Crash resilience: reopen windows after an unclean daemon death

  • Automation protocol: eval / goto / shot / wait-load / upload / scroll / focus

--background

/--headless

window modes (no focus steal, no window) - Measured benchmarks in docs (spawn, verification loop, memory)

  • Agent-ergonomic eval (expression or function body, probed server-side)
  • Sticky automation target: id-less commands follow the last-driven window

scroll

with selector/nth/contains disambiguation + landing report - Full-document screenshots ( shot --full

) - Persistent cookies (logins survive daemon restarts)

  • Text/a11y page snapshot ( hwatu snapshot

): token-cheap page state for agents - First-class interaction: hwatu click

/hwatu type

(selector or snapshot ref) - Console + network capture for verification loops ( hwatu console

) - Link hints

  • Profiles (separate cookie jars / web contexts); per-agent isolation
  • Displayless operation (nested headless compositor) for CI
── more in #developer-tools 4 stories · sorted by recency
── more on @hwatu 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/hwatu-a-daemon-based…] indexed:0 read:10min 2026-07-22 ·