Stillroom is an encrypted vault for the Mac. It keeps files, notes, passwords and two-factor codes, and it never goes online: no account, no server, no sync, no update check, no analytics.
Writing "we never connect to the internet" on a website is easy. Keeping it true is harder, because one dependency, one convenience call or one well-meant "check for updates" is enough to break it, and nobody notices until someone runs Little Snitch. So the claim is checked by the build. If the app binary can reach the network, the build fails and there is nothing to ship.
The check
It's a short shell script that runs against the compiled binary, not the source:
#!/bin/bash set -euo pipefail BIN="${1:?usage: check-offline.sh <path to the Stillroom binary>}" linked=$(otool -L "$BIN" | grep -E "/(Network|CFNetwork)\.framework/" || true) imported=$(nm -u "$BIN" | grep -E "^_(socket|connect|getaddrinfo|gethostbyname|sendto|bind|listen)$|NSURLSession|URLSession|NSURLConnection|NSURLDownload|_nw_|NWConnection|NWListener|NWBrowser|CFSocket|CFStreamCreatePairWith|CFHost|NSNetService|SCNetworkReachability" || true) if [ -n "$linked$imported" ]; then echo "check-offline: $BIN can reach the network:" >&2 [ -n "$linked" ] && echo "$linked" >&2 [ -n "$imported" ] && echo "$imported" >&2 exit 1 fi echo "check-offline: no networking code in $(basename "$BIN")" Two tools do the work.
otool -L lists the dynamic libraries the binary links. If Network.framework or CFNetwork.framework shows up, something pulled it in, and the build stops.
nm -u lists the undefined symbols, the functions the binary expects the system to provide at runtime. That catches the calls themselves: BSD sockets (socket, connect, bind, listen, sendto), DNS (getaddrinfo, gethostbyname), URL loading (URLSession, NSURLSession, NSURLConnection, NSURLDownload), the Network framework (NWConnection, NWListener, NWBrowser, anything _nw_), and the older Core Foundation and Bonjour ways in (CFSocket, CFHost, CFStreamCreatePairWith…, NSNetService, SCNetworkReachability).
Checking the binary rather than grepping the source matters. A Swift package I add, or a framework that quietly depends on URLSession, shows up in the binary whether or not I ever read its code.
Where it runs
The app is a Swift package with no Xcode project, built by a script. That script runs the check right after it assembles the app bundle, and the release script calls the same build script before signing and notarizing. So there is no path to a notarized download that skips it. It's the same on a normal debug build, which means I find out the moment I add something that touches the network, not on release day.
The one exception
Stillroom can show emails you've stored, and an email is HTML, so the viewer is a WKWebView. WebKit is networking code by definition, so this is the one place the check can't speak for, and it gets its own guard instead:
- JavaScript off, and clicked links cancelled.
- A Content Security Policy that allows only
data:. - A WebKit content block list that allows only
data:andabout:. -
<link>tags are disarmed before the page is shown, because preconnect and prefetch hints open connections on their own and the page policy doesn't cover them.
And a test proves it. It loads a hostile email that points every kind of remote load it can think of at a small server running on the same Mac: images, srcset, stylesheets, @import, fonts, frames, scripts, media, forms, SVG, <base>, meta refresh, preconnect, prefetch. The test fails if the server sees a single connection.
That test earned its place. At one point WebKit rejected one of the block list rules silently ("disjunctions are not supported"), and pages went out with one lock fewer than I thought. With the block list missing, the test server sees two connections, so now the email is never shown unless the block list compiled.
This is also why URLRequest on its own is allowed through the check: the email viewer reads the address of a clicked link in order to cancel it.
What it doesn't prove
The check catches mistakes. It isn't a proof, and I'd rather say where it stops than have someone find it:
- Some Foundation calls can load from the network through frameworks the check doesn't look at.
Data(contentsOf:)given anhttps://URL will happily fetch it. The rule in the code is to never pass one of those a URL that didn't come from a file. - It says nothing about macOS itself. The system does what it does.
- It's about the app. The website is separate: it has Cloudflare's cookie-free analytics, and the checkout runs on Paddle.
What never going online costs
There's no sync, which is the point, but it means you move a vault between Macs with a backup on a disk. There's no update check, so updates are a download from the website when you choose, and an RSS feed if you want to hear about them. The licence is an Ed25519-signed key checked entirely on the Mac, with no activation server, no clock and no expiry.
Those were deliberate trade-offs, and the build check is what keeps them from being undone by accident later.
If you make software with a similar promise, I'd like to know how you enforce it: link-time checks like this, an entitlement-based approach, or tests that watch the network. The threat model and file format are public at stillroomvault.com.

