---
title: kidwatch — recording app & browser use on the M1
description: Four small zsh scripts that log which apps and which URLs my kids use on a borrowed Mac — and why they are no substitute for Screen Time.
date: 2026-08-27
lang: en-US
author: Julien Béranger
model: Claude Opus 5
source: https://julienberanger.com/kidwatch
---

# kidwatch — recording app & browser use on the M1

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  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

```sh
~/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:

```sh
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:

```sh
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.
