Track your Claude Code sessions in ActivityWatch: 3-minute setup
Your Claude Code sessions run headlessly — no focused window, no keystrokes. ActivityWatch sees your desk as empty. Here's the 3-minute fix using aw-watcher-agent lifecycle hooks.
How to wire aw-watcher-agent into Claude Code's SessionStart and Stop hooks so your AI sessions show up in ActivityWatch's Timeline instead of as idle time.
Your Claude Code sessions run headlessly — no focused window, no keystrokes. ActivityWatch sees your desk as empty while your AI does the most valuable work of the day.
I wrote about why this happens and how aw-watcher-agent addresses it. This is the practical setup guide.
What you’ll see
After setup, each Claude Code session appears as a labeled block in the aw-webui Timeline: harness claude-code, model name, session ID, workspace, and duration. It sits next to your window-focus and AFK data, filling the visibility gap that headless execution creates.
Prerequisites
- ActivityWatch running locally (aw-server on
localhost:5600) aw-watcher-agentfrom gptme-contrib
Step 1: Install aw-watcher-agent
git clone https://github.com/gptme/gptme-contrib
pip install -e gptme-contrib/packages/aw-watcher-agent
Create the bucket (idempotent, safe to run multiple times):
aw-watcher-agent ensure-bucket
Step 2: Create the hook wrapper
Save this as ~/.claude/aw-hook.sh (adjust the BIN path if you installed into a different venv):
#!/usr/bin/env bash
# ActivityWatch hook for Claude Code SessionStart/Stop events.
# Non-fatal: a broken watcher never breaks the session it observes.
set -u
PHASE="${1:-}"
BIN="$(which aw-watcher-agent 2>/dev/null || echo '/usr/local/bin/aw-watcher-agent')"
LOG="/tmp/aw-watcher-agent-hook.log"
INPUT="$(cat 2>/dev/null || true)"
log() { echo "$(date -Is) $*" >>"$LOG" 2>/dev/null || true; }
read_field() {
printf '%s' "$INPUT" | python3 -c "
import json, sys
try:
d = json.load(sys.stdin)
except Exception:
d = {}
print(d.get('$1', '') or '')" 2>/dev/null || true
}
emit() {
[ -x "$BIN" ] || { log "no binary at $BIN"; exit 0; }
timeout 5 "$BIN" "$@" >>"$LOG" 2>&1 || log "emit failed (non-fatal): $*"
}
SID="$(read_field session_id)"
[ -n "$SID" ] || SID="${CC_SESSION_ID:-unknown}"
CWD="$(read_field cwd)"
WS="$(basename "${CWD:-workspace}")"
MODEL="${CC_MODEL:-unknown}"
case "$PHASE" in
start)
SRC="$(read_field source)"
TRIGGER="manual"
[ "$SRC" = "resume" ] && TRIGGER="resume"
emit emit-start --harness claude-code --model "$MODEL" \
--session-id "$SID" --trigger "$TRIGGER" --workspace "$WS"
;;
end)
emit emit-end --harness claude-code --session-id "$SID" --workspace "$WS"
;;
esac
exit 0
Make it executable:
chmod +x ~/.claude/aw-hook.sh
Step 3: Wire into Claude Code hooks
Add this to ~/.claude/settings.json (merge into the existing hooks key if it already exists):
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "~/.claude/aw-hook.sh start",
"async": true
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "~/.claude/aw-hook.sh end",
"async": true
}
]
}
]
}
}
Both hooks run async: true — they don’t block the session and their failures are silently swallowed. A down aw-server or missing binary costs you a missing Timeline block, not a broken session.
Verify it works
Start a Claude Code session and check the log:
tail -f /tmp/aw-watcher-agent-hook.log
You should see entries like:
2026-06-04T19:45:00 emit-start: session abc123 workspace myproject model claude-sonnet-4-6
Open ActivityWatch at http://localhost:5600 — your session should appear in the Timeline under bucket aw-watcher-agent_<hostname>.
Design notes
Three properties worth knowing about:
Local-only by construction. The watcher writes only to your own aw-server. No hosted aggregation, no transcripts — harness, model, session ID, duration. That’s it.
Zero heavy dependencies. The package uses a vendored stdlib REST client. No aw-client required. This is deliberate: a hook that drags a dependency tree is a hook you’ll think twice about running unconditionally.
One clean block per session. emit-start posts a zero-duration placeholder; emit-end replaces it with the real duration. You get one tidy Timeline block, no dangling intervals if a session dies mid-run.
The full design — bucket schema, event taxonomy, phased roadmap — is in the gptme-contrib repo.