A focus monitoring app for macOS — built for someone I love who wanted a way to stay on task, prioritize what matters during work sessions, and be held accountable.
Describe what you're working on, let AI break it into tasks, and Lockin tracks your app and browser activity in real time — flagging drift and showing how focused you actually were.
- AI task extraction — describe your work in plain English; Claude breaks it into prioritised tasks
- Lazy activity classification — apps and websites are automatically classified as on/off-task based on your session description, no preset lists needed
- Real-time drift alerts — get notified when you've been off-task too long
- Session timeline — a live activity feed and focus percentage while you work
- Post-session review — check off tasks and save as a reusable template
- Privacy-first — all activity is stored locally in SQLite; only the minimum context needed for classification is sent to the Anthropic API (see Data & privacy)
1. Describe your goal — tell Lockin what you're working on in plain English.
2. Review AI-suggested tasks — edit, reorder, and set priorities before locking in.
3. Focused session — live timer, on/off-task breakdown, and per-activity classification you can override.
4. Drift notifications — when you've been off-task too long, Lockin nudges you with a native macOS notification.
- macOS 12+ (Monterey or later)
- Node.js 18+ and npm 9+
- Xcode Command Line Tools — required to build the native accessibility module (
xcode-select --install) - An Anthropic API key (for AI features — get one here)
git clone https://github.com/jeanmw/lockin.git
cd lockin
npm installpostinstall runs electron-rebuild and then builds the native accessibility module at native/lockin-ax (macOS only; no-op elsewhere). If the native build fails (e.g. Xcode Command Line Tools aren't installed), npm install still succeeds and the app will run with browser URL tracking disabled. You can rebuild it later with:
cd native/lockin-ax && npm install && cd ../..resources/icon.png is committed, so you only need this if you want to regenerate it:
node scripts/generate-icon.jsnpm run devOn first launch you'll be prompted to grant Accessibility permission (required to read active window titles and browser URLs).
- Click Settings in the left sidebar
- Scroll to the AI section
- Paste your API key (
sk-ant-...) and press Enter
The key is stored locally in the app's SQLite database and only sent to the Anthropic API when making classification/suggestion requests.
Lockin needs two permissions on macOS. Both are prompted by the OS the first time the app tries to use them.
Required to read which app is in the foreground and to read browser URLs via the Accessibility API.
- System Settings → Privacy & Security → Accessibility
- Enable Lockin (or your terminal / Electron app during dev)
Required for browser URL fallback via AppleScript (when Accessibility doesn't return a URL).
- System Settings → Privacy & Security → Automation
- Enable Lockin (or your terminal / Electron app during dev) for the browsers you use (Chrome, Safari, Arc, etc.)
- "Grant Access" prompt won't go away. Toggle the permission off and on in System Settings, then restart Lockin. During development, macOS ties Accessibility permission to the specific binary — rebuilding with a new signing identity can reset the grant. Try removing the old entry and re-adding the app.
- Browser URL shows empty. Automation permission is missing, or the native module didn't build. Check the main-process console for
[ax-reader] Native module not available.— if you see it, rerun the native build step from Setup. - Permissions modal re-appears on every launch. The app bundle lost its signing identity (common with unsigned local builds after a clean). Re-grant the permissions once and they'll persist until the next unsigned rebuild.
- Nothing is tracked at all. The
ActivityMonitorusesosascriptto query the frontmost app, which requires Automation permission for each target app (not just a single global grant). Open Settings → Permissions to see which grants are missing.
Lockin tries to be frugal with API calls. Classification is lazy — it only happens when needed, and the result is cached as a rule so the same app/site is never re-classified.
- During a session, every activity tick records the foreground app and (for browsers) the URL.
- The activity is checked against stored rules (
app_rulestable) — project-specific rules first, then global rules. - If no rule matches, the domain or app bundle ID is enqueued for classification.
- A background worker drains the queue one job at a time, calling Claude Haiku with:
- Your session description
- Your session's task descriptions
- The domain (e.g.
github.com) or the app name + bundle ID
- The classification result (
on_task/off_task/neutral) is stored as aninferredrule.neutralresults are not stored — the app stays neutral until you classify it manually. - Rules you create or edit manually are
source: 'user'and take precedence over inferred ones.
Window titles are only sent to the API in a separate flow (task attribution — matching current activity to one of your session tasks), and only when task attribution is enabled.
Domains are reduced to their effective root before being stored as rules (app.hubspot.com → hubspot.com), except for a small allowlist where subdomains are meaningful (*.amazonaws.com, *.github.io, etc.).
See src/main/services/AIService.ts for the full implementation.
npm test # run once
npm run test:watch # watch mode
npm run typecheck # TS type-check onlyTests use Vitest. Currently covered: AlertEngine, Classifier, LearningService. Areas without tests (SessionManager, ActivityMonitor, MetricsService, TaskService, AIService) are good first contribution targets — see CONTRIBUTING.md.
src/
main/ Electron main process
services/ ActivityMonitor, SessionManager, AIService, AlertEngine …
db/ Drizzle ORM schema + migrations (SQLite)
ipc/ IPC handler registry
native/ Loader for the native accessibility module
preload/ Typed contextBridge bridge
renderer/ React + Tailwind UI
screens/ SessionSetup, LiveFocus, Dashboard, Settings
components/ Sidebar, FocusTimer, AlertOverlay, Logo …
hooks/ useSession, useTasks, useAlerts …
shared/ Zod IPC types + constants
native/
lockin-ax/ Native macOS accessibility module (Objective-C++ / N-API)
scripts/ Build helpers (icon generator, native build, notarization)
npm run build # compile main + renderer
npm run package # package as .dmg and .zip for arm64
npm run publish # build + upload to GitHub Releases (requires GH_TOKEN)Signing and notarization are configured in package.json under build and scripts/notarize.js. See .env.example for the required APPLE_* environment variables.
Stored locally:
- All session, activity, rule, and task data lives in a SQLite database at
~/Library/Application Support/Lockin/lockin.db. - Your Anthropic API key is stored in the same database as plaintext inside the
user_settingstable. This is a known limitation; a migration to Electron'ssafeStorage(macOS Keychain-backed) is planned. If this matters to you, avoid using a key tied to a high-limit billing account. - You can wipe everything from Settings → Data → Clear all data.
- If you used an older build that stored the DB at
~/.config/lockin/lockin.db, the app will automatically copy it to the new location on first launch (the legacy copy is left in place — delete it manually once you've confirmed the migration worked).
Sent to the Anthropic API (only during active sessions, only when AI features are enabled):
| When | What is sent |
|---|---|
| Task extraction (session start) | Your session description |
| Rule suggestions (project setup) | Your project description |
| Lazy classification (new app or domain) | Session description, session task descriptions, and the domain or app name + bundle ID |
| Task attribution (optional) | Current app name, bundle ID, domain, window title, and your task list |
See Anthropic's privacy policy for how they handle API requests.
Never sent:
- Keystrokes, screen contents, file contents, clipboard contents.
- Activity data outside of an active session.
- Anything when AI classification is disabled in Settings.



