kidwatch — recording app & browser use on the M1

Julien Béranger

+ Claude Opus 5

Setup for lending the Mac to a 10 and a 12 year old. Records which apps are used and which URLs are visited in Chrome, then summarizes it.

All scripts live in ~/kidwatch/. Tested on macOS 14.5 (Apple Silicon), stock zsh/awk/sqlite3 — no dependencies to install.

Tell the kids the machine logs activity. It works better as a stated rule than as a secret, and it avoids having to explain later where the list came from.

The four pieces

FileWhat it does
~/kidwatch/kidwatch.shThe recorder. Samples the frontmost app + active browser tab every 10s.
~/kidwatch/kidwatch-report.shHuman-readable summary: time per app, per site, top pages, timeline.
~/kidwatch/chrome-history.shReads Chrome's own history DB — catches what the poller misses.
~/kidwatch/install.shInstalls the recorder as a login agent (starts automatically).

kidwatch.sh — the recorder

Every 10 seconds it logs the frontmost application. If that app is a browser, it also grabs the active tab's URL and title.

  • Covers Chrome, Brave, Edge, Chromium, Vivaldi and Safari. Safari matters — switching browsers is the obvious workaround.
  • Skips samples while the screen is locked or after 2 minutes of no input, so totals mean "actually using it", not "left the lid open".
  • Writes tab-separated logs to ~/kidwatch/logs/YYYY-MM-DD.tsv (timestamp, app, url, title, seconds).

Tunable via env vars: KIDWATCH_INTERVAL (default 10s), KIDWATCH_IDLE_MAX (default 120s), KIDWATCH_DIR (default ~/kidwatch).

kidwatch-report.sh — the summary

Total screen time, minutes per app, minutes per website, top pages with page titles, and a timeline of activity blocks. Example output:

  Total active screen time: 55 min (0.9 h)

── TIME PER APP ──
  Google Chrome        30.0 min  (54.1%)
  Minecraft            20.0 min  (36.0%)
  Safari                4.2 min  ( 7.5%)

── TOP PAGES ──
    20.0 min  Minecraft video
              https://www.youtube.com/watch?v=abc

── TIMELINE ──
  14:00:00 → 14:14:50   15.0 min  Google Chrome
  14:15:00 → 14:34:50   20.0 min  Minecraft
  15:30:00 → 15:34:00    4.2 min  Safari

Page titles are kept deliberately: without them every YouTube video collapses into a single anonymous youtube.com/watch row.

chrome-history.sh — the complement

Reads Chrome's own SQLite history database (all profiles, working on a copy so Chrome's lock doesn't matter). This is the important complement: the poller only sees the active tab every 10 seconds, so it misses background tabs and quick visits. This catches everything, and adds:

  • sites ranked by visit count
  • the full visit log, newest first
  • downloads (often the interesting part)
  • search queries, percent-decoded, filtered to real search engines

Running it

~/kidwatch/kidwatch.sh &          # start recording
~/kidwatch/kidwatch-report.sh     # today's summary
~/kidwatch/kidwatch-report.sh 2026-08-27   # a specific day
~/kidwatch/kidwatch-report.sh --all        # everything logged
~/kidwatch/chrome-history.sh 24   # last 24h of Chrome history
~/kidwatch/install.sh             # start automatically at login

Stop the login agent:

launchctl unload ~/Library/LaunchAgents/cc.strat.kidwatch.plist

Run it by hand once before installing the agent

macOS will prompt to let it control System Events and Chrome. Those prompts don't reliably appear for launchd jobs — skip this step and the log fills with app names but no URLs. You may also need to add Terminal under System Settings → Privacy & Security → Accessibility.

Things worth knowing

Use a separate macOS account for the kids and install there. Two reasons: the log stays scoped to them, and chrome-history.sh run on your own profile dumps your browsing — during testing it printed session tokens and personal browsing. A separate account also means a separate Chrome profile, so the histories don't mix.

Incognito defeats the history script. The poller still catches it (it reads the live window), but to disable incognito in Chrome outright:

sudo defaults write /Library/Preferences/com.google.Chrome IncognitoModeAvailability -integer 1

This records, it does not block. For a 10 and a 12 year old, macOS Screen Time (Content & Privacy Restrictions) and Chrome's Family Link supervision do the actual limiting — time caps, app restrictions, adult-content filtering — and they can't be sidestepped by quitting a script. These scripts are the right tool for seeing what happened; they are not a substitute for the guardrails. Run both.

Implementation notes

Two things that bit during development, worth knowing before editing the scripts:

  1. AppleScript can't tell application <variable>. Terminology is resolved at compile time, so tell application frontApp ... URL of active tab fails to compile and the whole script silently returns empty. Each browser needs its own literal tell application "Google Chrome" block.
  2. macOS ships BWK awk, not gawk. No asorti, no mktime. Sorting goes through sort -rn pipelines and timeline math uses seconds-of-day arithmetic parsed from the timestamp instead.

Chrome stores timestamps as microseconds since 1601-01-01, hence the /1000000 - 11644473600 conversion in the SQL.