← back

CASE STUDY

MirrorAgent

Reads your screen every 5s. At most one model call a minute.

A self-hostable macOS focus assistant that watches what you're actually doing and calls it out, using vision instead of blocklists.

  • Electron
  • React
  • TypeScript
  • Claude Vision

The problem

Blocklist-based focus tools fail in both directions. They block the documentation you need and miss every new way to waste an hour, because a blocklist can only match a URL — it has no idea what you're doing with it. YouTube is a distraction until the thing you're stuck on has a good conference talk about it.

MirrorAgent classifies the activity instead of the address. It watches the screen, asks a vision model what's actually happening, and acts on the answer. Everything stays on the machine, and you run it with your own API key.

The loop

Every 60 seconds — or immediately when you switch apps — it captures the active display and sends the screenshot to claude-haiku-4-5 along with the last ten window entries and your past corrections. The classification has three outcomes:

  • Work — nothing happens.
  • Unsure — a passive notification: "Is this work?"
  • Distraction — a 30-second warning countdown, then a hard block.

Blocking hides the app through the macOS Accessibility API rather than force-quitting it, so nothing is ever lost. For browsers, hiding the whole window is too blunt — a companion Chrome extension holds a WebSocket on localhost:1423 and closes the specific offending tab, keeping the rest of the session intact. It uses chrome.alarms to keep the service worker from being reaped and reconnects every three seconds if the socket drops.

Not sending every frame to a model

Window state is observed every five seconds, but frames only reach the model when they have to. Three mechanisms do that work, and together they're where the cost reduction comes from:

A gate before the model. Anything on the permanent blocklist, the whitelist, or the configured work-apps list resolves locally and instantly. Known apps never cost a call — the model only sees genuinely ambiguous situations.

A hard rate limit. At most one classification per minute, with a minimum 55-second gap enforced in the classifier. Observation runs at 5-second granularity, so the observation-to-call ratio is 12:1 before the gate rejects anything.

A small model. Haiku, not a frontier model. This is activity classification with strong context, not a reasoning problem.

Correction memory makes each of those cheaper over time. Every "this is work" and "block it" is stored in a correction_profile table and injected into later prompts, so the same ambiguous app stops being ambiguous. Alongside it, a confidence threshold ramp starts at 85% for the first three days and relaxes to 70% by day seven — the system is deliberately timid before it has learned your patterns, because a false block on day one gets the app deleted.

Local by default

Anyone installing something that screenshots their desktop will look for this section first, so: everything is local. SQLite via Drizzle at ~/Library/Application Support/MirrorAgent/, holding window observations, block events, corrections, and daily scores. There is no server, no account, and no telemetry. The only thing that leaves the machine is a screenshot going to Anthropic under your own API key, and only when the gate and rate limiter both allow it.

Sleep and lock stop the observer and classifier outright; they resume on wake.

The score

A daily 0–100 focus score across four components: focus ratio (40 points), block resistance (30), distraction depth (20), and consistency (10). Block resistance is the interesting one — it measures whether you respected your own blocks, which is the number that actually predicts whether the tool is working. A local dashboard at localhost:1422 shows today, a seven-day chart, and live-editable settings.

Because almost none of this is unit-testable — it's notifications, tray state, window hiding, and sleep transitions — there's a manual test suite in the dashboard instead: 30 tests across 10 groups, with auto-checks for things like rate-limiter state and gate behaviour, and trigger buttons to fire every notification, HUD nudge, pause transition, and simulated sleep/wake by hand.

Stack: Electron 33 · Vite · React 18 · TypeScript · Tailwind · SQLite · Drizzle · Anthropic SDK · Express · ws

What I'd change

The build isn't signed or notarized. hardenedRuntime and entitlements are configured, but there's no signing identity, so installation currently requires xattr -cr to clear the quarantine flag — which is exactly the instruction that should make someone suspicious of an app that reads their screen. An Apple Developer certificate and a notarization step are the difference between a real distributable and a thing only I run, and it's the next work.

The 5-second observation cadence is also a guess rather than a measurement. It was picked to feel responsive and has never been tested against a slower interval to see whether accuracy actually drops.