My kids do Math Academy on their own laptops. It’s a browser app, which creates the usual parenting problem: I want them in that one site and nowhere else. A learning tool shouldn’t be a doorway to the whole internet.
The setup I’d landed on was clever, or so I thought. Chrome lets you take a web app and “install” it as a standalone window — no address bar, no tabs, just the app. macOS Screen Time can then allow that specific app while blocking the full Chrome browser. In theory: Math Academy stays open, everything else stays shut.
In practice, it leaked.
The leak
Every so often, a link inside Math Academy would try to open in a new window — a target="_blank" link, or something calling window.open. The standalone app-shell can’t open a second window inside itself, so Chrome does the sensible-but-wrong thing and hands the link off to the full browser. Which is blocked. Which trips Screen Time. Which throws up the passcode prompt.
Now I’m stuck with two bad options. Leave it blocked, and my kid is staring at a permission wall in the middle of a math lesson. Unblock Chrome so the link resolves, and the whole containment strategy is gone — they’ve got the open internet.
The frustrating part is that the link usually went right back to Math Academy. It wasn’t an escape attempt; it was a different Math Academy URL that happened to be tagged to open in a new window. The containment was failing on a technicality.
The wrong layer
My first instinct was to shop for a better tool. There’s a whole genre of freeware “focus” apps — Cold Turkey, SelfControl, and friends — that promise granular blocking. I started pricing out which one to install.
Then I noticed the shape of what I actually wanted, and it was backwards from what those apps do. Blockers are built around a blocklist: you name the bad sites and it keeps them out. I didn’t want to name the bad sites. There are infinitely many bad sites. I wanted an allowlist with exactly one entry — Math Academy — and a hard wall around it.
That reframing matters, because it tells you the containment belongs at the layer where the navigation actually happens: inside the browser, on the page itself. Screen Time was operating one level too high. It could see “a new Chrome window appeared” but it couldn’t see that the window was harmless. All it had was an all-or-nothing switch, so I kept getting all-or-nothing outcomes.
So instead of a bigger hammer, I described the problem to Zephyr — the agent that runs our house — and asked for the smallest possible fix. A few minutes later I had a forty-line Chrome extension.
The fix
It’s a Manifest V3 content script scoped to mathacademy.com and nothing else. It does three small things:
- Rewrites
target="_blank"links to open in the same window. The pop-out never happens, so the hand-off to full Chrome never happens, so Screen Time never fires. - Neutralizes
window.openthe same way — if a script tries to pop a Math Academy URL, it just navigates in place; if it tries to pop anything else, the call is dropped. - Blocks clicks to any non-
mathacademy.comdomain. This is the bonus: it turns the extension into a soft surfing wall inside the app itself. Even if a stray external link shows up, it goes nowhere.
Here’s the whole thing:
// Math Academy Shell Guard — keeps navigation inside Math Academy.
(() => {
const HOST_RE = /(^|\.)mathacademy\.com$/i;
const inScope = (url) => {
try { return HOST_RE.test(new URL(url, location.href).hostname); }
catch { return false; }
};
// 1) Neutralize window.open — in-domain pop-outs navigate in place,
// off-domain pop-outs are dropped.
window.open = function (url) {
if (url && inScope(url)) location.assign(new URL(url, location.href).href);
return null;
};
// 2) Intercept clicks. In-domain links stay in the same window;
// off-domain links are cancelled.
document.addEventListener('click', (e) => {
const a = e.target && e.target.closest && e.target.closest('a[href]');
if (!a) return;
const href = a.href;
if (!href || href.startsWith('javascript:')) return;
if (inScope(href)) {
if (a.target && a.target !== '_self') { e.preventDefault(); location.assign(href); }
} else {
e.preventDefault();
e.stopPropagation();
}
}, true);
// 3) Belt-and-suspenders: rewrite target=_blank as anchors appear.
const strip = (root) => {
if (root.querySelectorAll) {
root.querySelectorAll('a[target]').forEach((a) => {
if (a.target !== '_self') a.target = '_self';
});
}
};
const startObserve = () => {
strip(document);
new MutationObserver((muts) => {
for (const m of muts) for (const n of m.addedNodes) if (n.nodeType === 1) strip(n);
}).observe(document.documentElement, { childList: true, subtree: true });
};
if (document.documentElement) startObserve();
else document.addEventListener('DOMContentLoaded', startObserve);
})();
The manifest is just as short — it points the script at mathacademy.com, runs it at document_start, and sets "world": "MAIN" so the window.open override lands before the page’s own scripts get to it.
Load it once per laptop via chrome://extensions → Developer mode → Load unpacked, and the Screen Time dance disappears. The kids get Math Academy. Nothing else gets a door.
The part worth generalizing
There’s an honest caveat: loaded this way, a determined kid could open the extensions page and remove it. For that there’s a heavier version — a managed-policy force-install that pins the extension so it can’t be toggled off — and I’ll reach for it if it ever becomes a game. But that’s a hardening step, not the fix. The fix is forty lines.
What I keep coming back to is how close I came to installing a whole parental-control suite for a problem that a page-level script solved cleanly. The blocker app would have “worked,” in the sense that it would have added a second heavy system on top of the one already misbehaving. It would not have addressed the actual thing, which was a link opening in the wrong window.
The generalizable lesson is boring and I believe it more every month: match the fix to the layer where the problem lives. The leak was on the page, so the fix belonged on the page. When you find yourself shopping for a bigger tool, it’s worth one hard look to check whether you’re about to paper over the real issue with something impressive instead of solving it with something small.
The other lesson is quieter, and it’s the whole reason we do what we do here. I didn’t write this extension. I described an annoyance out loud to an agent living on a Mac mini in my house, and a working, scoped tool came back before I’d finished being annoyed. That’s the household-Mind thing in miniature: not a chatbot, not a demo — just the friction going away.