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
| File | What it does |
|---|---|
~/kidwatch/kidwatch.sh | The recorder. Samples the frontmost app + active browser tab every 10s. |
~/kidwatch/kidwatch-report.sh | Human-readable summary: time per app, per site, top pages, timeline. |
~/kidwatch/chrome-history.sh | Reads Chrome's own history DB — catches what the poller misses. |
~/kidwatch/install.sh | Installs 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 SafariPage 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 loginStop the login agent:
launchctl unload ~/Library/LaunchAgents/cc.strat.kidwatch.plistRun 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 1This 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:
- AppleScript can't
tell application <variable>. Terminology is resolved at compile time, sotell application frontApp ... URL of active tabfails to compile and the whole script silently returns empty. Each browser needs its own literaltell application "Google Chrome"block. - macOS ships BWK awk, not gawk. No
asorti, nomktime. Sorting goes throughsort -rnpipelines 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.