Menu
Projects

Open sourcePersonal projectmacOS

Null

A browser where nothing is sent, nothing is stored and nothing is tracked, unless you explicitly choose otherwise.

Null, showing pinned sites and tabs in the left source list and recent notes as cards on the new-tab surface

Pinned sites and tabs in the left source list, recent notes as cards on the new-tab surface. The whole window is glass over macOS vibrancy.

Open source under MPL 2.0. macOS first. Tauri 2 + Rust + React, using the system WebView rather than a bundled engine. Not funded, not monetised, not for sale.

In simple terms

Null is a web browser I wrote for macOS. The name is the thesis: null is what a function returns when there is nothing to return, and that is the right default for a browser.

There is no account system, no sync service and no telemetry endpoint. It does not phone home on launch, does not check for updates unless asked, and sends no crash reports anywhere. Bookmarks, history, notes and settings sit on your own machine in files you can open with a text editor.

A panel lists every outbound request a page makes, grouped by origin, and you can block any origin from it. Blocked requests are still logged, so you see what was refused rather than only what got through.

The part I want on record: it shipped with a full AI layer, and I took it out.

The impact. Six rules about what the app may never do are enforced in the code and checked at review, so the privacy claim is a property of the build rather than a page in a policy. The strongest product decision in it was deleting a feature I had already shipped.

Why removing the AI made it a better browser
  • The feature was real, not a stub

    Local models by default, your own API key held in the system keychain, chat grounded in the tab you were reading, summarise. It worked, and people liked it.

  • It quietly broke the one promise

    A browser that can read the page for you is a browser with a reason to send the page somewhere. Every safeguard I kept adding was managing a risk that only existed because the feature did.

  • The useful half survived without it

    What people actually wanted from the AI layer was to keep what they read. That is now page capture and notes written to markdown on your own disk, which needs no model at all.

  • It made the rules enforceable

    Once nothing in the app wanted to send anything, "sends nothing by default" stopped being an aspiration and became an invariant you can fail a code review over.

What it is

The name is the thesis. null is what a function returns when there is nothing to return, and that is the correct default for a browser. Null is a macOS desktop browser built on Tauri 2 in Rust with a React and TypeScript interface, rendering through the system WebView, so pages look the way they would in Safari while the browser around them is written to different defaults.

There is no account system, no sync service and no telemetry endpoint. It does not phone home on launch, does not check for updates unless asked, and ships no crash reports anywhere. Bookmarks, history, notes and settings live on the machine in SQLite, plain markdown and localStorage, readable with sqlite3, grep or any text editor.

The part I want on record: it had AI, and I removed it

Null shipped a full AI layer. Local Ollama by default, bring-your-own Anthropic key in the OS keychain, chat grounded in the current tab, summarize, multi-turn conversation history in SQLite. Four milestones of work. It is gone, and the milestone list in the repo says so rather than pretending it never happened.

The reasoning is short. A browser that holds an API key is a browser you have to trust, and after living with it the only part that consistently earned its place was the part that needed no model at all: getting a clean copy of the page out of the browser and into somewhere else. So that is what is left. Null captures; you decide what reads it.

Invariant 3 was rewritten from “inference is local by default” to no inference in the browser, and migration 006 drops the conversation tables. Putting a model back, local or remote, now needs a decision recorded in PHILOSOPHY.md before any code. Deleting working software you spent months on is the judgment I am most pleased with on this project.

The six invariants

Not defaults. Invariants: code that violates one is a bug, and every pull request that touches networking or storage answers three questions from the diff alone. What does this store? What does this transmit? What does this remember?

  1. Zero telemetry. No analytics, no crash reporting to a server, no usage statistics, no phone-home of any kind.
  2. No default cloud connections. It must start up and browse without contacting any service beyond the site you asked for.
  3. No inference in the browser. It does not run or call a language model. It captures pages as markdown; you take that markdown wherever you like.
  4. Every outbound connection is visible. Through the Network Inspector, in real time, grouped by origin.
  5. Data lives with you. SQLite and plain markdown on disk. No mandatory sync, no cloud account.
  6. No dark patterns. No forced onboarding, no retention tricks, no notification spam.

Radical transparency, as a surface

The Network Inspector is a first-class panel rather than something buried in devtools. It streams every outbound request the browser makes, grouped by origin, and a shield next to any origin cancels its future requests at the webview layer. Subresources to a blocked origin still log, marked blocked, so you can see what was refused rather than only what got through. The buffer is capped and never persisted; the blocklist is the only part that survives a restart.

The Network Inspector listing four requests across three origins, with google-analytics.com struck through and marked blocked
The Network Inspector, mid-page-load. Four requests, three origins, one blocked: google-analytics.com is struck through and cancelled at the webview layer, and still logged so you can see what was refused.
A YouTube video playing with the Notes panel docked to its right, holding a note titled Zizek that is bound to the video URL
Notes docked beside the page it is about, not on top of it. The note is bound to the URL in its header, so returning to the video returns the note with it, and the row above carries copy, preview and delete.
A note card open beside the page, with saved pages and selections listed and a copy button on every row
The same panel, listing what has been kept. Save a page or a selection as markdown, annotate it, copy it out. Every note is written twice: once to SQLite as the index, once to a real file in ~/Documents/Null.
The new tab: a radial graph of thirteen visited sites around a centre point, with two note cards listed underneath
The new tab is the profile looking at itself. Thirteen sites as a graph rather than a wall of thumbnails, and under it the notes, each labelled with the page it came from and when it was last touched.

Capture, not inference

Notes is where the browser earns its keep. A note opens beside the page rather than covering it, autosaves as you type, and carries the page's URL as its source line. Saving a page runs Mozilla Readability and Turndown to get the article as markdown; saving a selection converts whatever is highlighted. Both run inside the tab's own WebView with vendored copies, so no network call of any kind is involved.

Every note is written twice: to SQLite, which is the index the list reads, and to ~/Documents/Null/ as a real markdown file with YAML front matter. The file is the copy that matters. It opens in Obsidian, it greps, and it is still readable long after you stop running Null. The sync goes both ways, and it refuses the dangerous cases: a file older than Null's own last write is treated as a stale mirror rather than an edit, and an empty file never erases a note, because editors save by truncate-then-write and reading mid-save must not eat the copy.

The engineering worth pointing at

  • Chrome height is a constant. Opening a second tab or saving a first bookmark used to change it, which physically reflowed the page you were reading. Tab creation and tab resizing now consume one function, contentRect(), so they cannot disagree about where the page goes.
  • Shortcuts are native menu accelerators rather than listeners in the shell. Not cosmetic: the shell and each tab are separate native webviews, so a shell listener stops receiving keys the moment you click into a page, which is exactly when Reload and Back matter.
  • The privileged webview is pinned to its own origin. A strict CSP on the shell, http/https-only tab navigation refused at the IPC boundary, and a navigation guard so a remote link surfaced inside the shell can only ever open in a tab. Written up in SECURITY.md.
  • Favicons are captured, never fetched. Taken from pages as you visit them and validated in Rust, because fetching them would breach invariant 2. A hostname-derived letter mark stands in until the first visit.
  • Extraction rides an image beacon. Readability and Turndown run in the tab and return through a custom null-event:// scheme as chunked Image.src requests rather than fetch, because img-src is broad where connect-src is locked down on exactly the sites worth clipping.
  • Split view and pin folders. Drag a tab or a pin out of the sidebar onto the page for a live drop target; drop one pin dead-centre on another to fold them, iOS-style. Deleting a folder re-roots its pins, so arrangement is never a place data can be lost.
  • Session restore that costs nothing. Restored tabs come back dormant, as rows with no webview, and load when selected. Twenty restored tabs cost one page load.

Where it is

M0 through M2, M6, M7 and M8 are done: browsing, bookmarks and history; the Network Inspector with subresource capture and per-origin blocking; shell hardening; the sidebar navigation rebuild; and the Zen-informed redesign that brought glass, split view, the Notes editor, pin folders, popups, downloads, per-tab zoom and find.

Next is subresource blocking through WebKit's own WKContentRuleList, a command bar that searches notes, bookmarks and history together, and FTS5 search over what you have actually seen. A SearXNG provider already exists in the Rust backend with no interface in front of it, because the search view was part of the AI drawer that was removed. It gets a UI or it gets cut.

Tauri 2 · Rust · React 19 · TypeScript · Tailwind v4 · SQLite · WebKit · MPL 2.0

What Null is not

Not a Chromium fork, because one maintainer cannot keep up with Chromium. Not an AI browser: it captures, it does not think. Not a product, not funded, not monetised, not for sale. Not a competitor to Chrome or Safari or Firefox, and it does not need to displace them to matter. Not for everyone. It is for people who would rather have control than convenience.