Every tool on Toggle9 has a small chip under its title: Runs in your browser. It's an easy thing to write and a hard thing to prove. A chip doesn't stop a script from phoning home, and "we respect your privacy" banners are cheap.
So I tested it. On 26 October 2026 I loaded every published tool page, all 200 of them, in a fresh headless browser and logged every request each page made. Then I did it again with Global Privacy Control switched on, and a third time with the network cut.
This post covers the script, what it found, the one tool where my own label was wrong, and a 35-line version you can point at your own site.
What "runs in your browser" is supposed to mean
For a calculator, it comes down to four things:
- The maths runs on your device. The page downloads the tool's JavaScript, and every answer is worked out in your browser as you type. There's no "Calculate" button that posts your numbers to a server.
-
Your inputs live in the URL fragment. Type a loan amount and the link becomes
.../loan-calculator/#a=250000&r=6.5. Refresh or press Back and your inputs come back. Browsers never send the part after#: RFC 3986, section 3.5, says the fragment is "separated from the rest of the URI prior to a dereference" and handled "solely by the user agent". -
Recent calculations go in
localStorage, which, unlike a cookie, is never attached to a request. - Analytics, where it's on, gets the page address and nothing you typed.
Each of those is a claim about network traffic, so you can test it by watching the network.
The test
The audit is a Node script using Playwright with Chrome's headless shell. For each published tool page it opens a fresh browser context (no cookies, no storage) and runs three passes.
Pass 1, normal load. It records every request for 5 seconds after load. The URL carries a test word in the fragment, #auditprobe=hashtest123. If that word shows up in any request URL or POST body, something is reading the fragment and sending it on.
page.on('request', r => reqs.push({ url: r.url(), post: r.postData() || '' }));
await page.goto(url + '#auditprobe=' + PROBE, { waitUntil: 'load', timeout: 45000 });
await page.waitForTimeout(5000);
// ...
leak: reqs.some(r => r.url.includes(PROBE) || r.post.includes(PROBE))
It also decodes the dl (document location) parameter of every Google Analytics hit and checks it has no # in it.
Pass 2, Global Privacy Control on. GPC is a browser signal (navigator.globalPrivacyControl === true) that means "don't sell or share my data". Brave, DuckDuckGo and Firefox (with a setting) send it. Playwright has no switch for it, so the script fakes it before any page script runs:
await ctx.addInitScript(() =>
Object.defineProperty(Navigator.prototype, 'globalPrivacyControl', { get: () => true }));
Pass 3, offline. Load the page, cut the network with context.setOffline(true), type 37 into the first number box, and check the answer changed. If the maths really runs locally, losing the network shouldn't matter.
await ctx.setOffline(true);
const before = await p.locator('.t9-tool').innerText();
await inp.fill('37'); await inp.dispatchEvent('input');
await p.waitForTimeout(1200);
return before === (await p.locator('.t9-tool').innerText()) ? 'NO CHANGE' : 'updated';
Four pages run in parallel, from a laptop.
What it found
| Where the pages connected | Pages, of 200 | With GPC on |
|---|---|---|
| toggle9.com (the page, its code, fonts, images, data files) | 200 | 200 |
| googletagmanager.com (loads Google Analytics) | 200 | 0 |
| google-analytics.com (the visit count) | 200 | 0 |
| Anywhere else | 0 | 0 |
-
The
#test word reached no server on any page, and no Analytics hit carried a fragment. - With GPC on, 0 pages contacted Google.
- Offline: 128 of 129 tools with a number box kept updating. The one that didn't is the crossword solver, which fetches its word lists when you search. That's expected, but it's a good reminder that "runs in your browser" doesn't always mean "works without a connection".
No ad networks, no font CDNs, no third-party error trackers. The fonts are served from the site itself, so there's no Google Fonts request either.
How GPC is handled
The analytics loader checks the signal first. If it's set, it clears any old _ga cookies and returns before Google's script is ever added to the page:
function gpc() { return navigator.globalPrivacyControl === true }
if (gpc()) { clear(); return; } // never load Analytics, wipe old _ga cookies
var c = get(); // the visitor's saved choice, if any
if (c === 'denied') return;
if (c === 'granted') { load(); return; }
// No choice yet: ask first in the EU, EEA, UK and Switzerland, otherwise load
The important part is the order. GPC wins over everything, including a "yes" saved earlier, so a visitor who turns the signal on later doesn't have to find a settings page.
The tool where my own label was wrong
Not every tool can run entirely on your device. A DMARC checker has to ask DNS for a domain's records, and a mileage calculator needs a road distance from a routing service. Those tools are meant to replace the default line, "Nothing you type leaves this page", with a specific one: what they send, when, and to whom.
When I built the page explaining the chip, I went through every tool's line against what its code actually fetches. The mileage calculator was still showing the default. It sends the two places' map points (rounded to about 11 m) to my server, which asks openrouteservice for the distance. The label said nothing left the page.
Nothing was hidden, since the request was right there in the network tab, but the label was false. I fixed it and logged it as a correction. The build now generates a trust file from every tool's engine, so a tool that fetches anything has to declare what it sends, or it doesn't get the default line.
The lesson I took from it: the network test proves what happens when a page loads, not when you press every button. The audit waits 5 seconds and types into one box. Lookups that only fire on a "Check" button need a second source of truth, and for me that's each tool's own declaration, listed on one page.
What this doesn't prove
To be clear about the limits:
- It's my test, not an outside audit. Nobody independent has checked Toggle9.
- Loading a page is still a request. Cloudflare and my server see your IP address and which page you opened, like any website.
- A fragment is private, not secret. It stays in your browser history, and anyone you share the link with sees your inputs.
- Browser extensions can read any page you open. "Runs in your browser" can't protect you from your browser.
Run it on your own site
Here's a stripped-down version you can point at any URL. It lists every host a page contacts in its first 5 seconds, with GPC off and on, and flags it if the # test word reaches a server.
// npm i playwright && npx playwright install chromium
// node who-does-it-call.mjs https://example.com/page [more URLs...]
import { chromium } from 'playwright';
const PROBE = 'hashprobe' + Date.now();
const browser = await chromium.launch();
async function hosts(url, gpc) {
const ctx = await browser.newContext();
if (gpc) await ctx.addInitScript(() =>
Object.defineProperty(Navigator.prototype, 'globalPrivacyControl', { get: () => true }));
const page = await ctx.newPage();
const seen = {}; let leaked = false;
page.on('request', r => {
if (r.url().includes(PROBE) || (r.postData() || '').includes(PROBE)) leaked = true;
const u = new URL(r.url());
if (u.protocol.startsWith('http')) seen[u.host] = (seen[u.host] || 0) + 1;
});
await page.goto(`${url}#probe=${PROBE}`, { waitUntil: 'load' });
await page.waitForTimeout(5000);
await ctx.close();
return { seen, leaked };
}
for (const url of process.argv.slice(2)) {
for (const gpc of [false, true]) {
const { seen, leaked } = await hosts(url, gpc);
console.log(`\n${url} (GPC ${gpc ? 'on' : 'off'})${leaked ? ' ⚠ # word reached a server' : ''}`);
for (const [h, n] of Object.entries(seen).sort((a, b) => b[1] - a[1])) console.log(` ${String(n).padStart(3)} ${h}`);
}
}
await browser.close();
On one of my pages it prints:
https://toggle9.com/compound-interest-calculator/ (GPC off)
20 toggle9.com
2 www.google-analytics.com
1 www.googletagmanager.com
https://toggle9.com/compound-interest-calculator/ (GPC on)
19 toggle9.com
Try it on a site you use every day. The list is often longer than you'd guess.
The full results for every tool, with each one's own line about what it sends, are on What "runs in your browser" means.
Have you ever run something like this on your own site? I'd like to know what turned up, especially anything you didn't expect to find.
Top comments (0)