This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady
What I Built
Le Laissez-Log is a small terminal-session watcher for ActivityWatch, the open-source, local-first time tracker.
ActivityWatch already knows when you are at the keyboard (aw-watcher-afk) and which window is active (aw-watcher-window). What it doesn't know is what you actually did in the terminal, which is where a lot of daily learning happens: the commands you tried, the errors you hit, the fix that finally worked.
My watcher wraps your shell in a PTY and writes one ActivityWatch event per command: the command, how long it ran, and a short, redacted snippet of its output. You keep working as usual. The log builds itself in the background, so at the end of the day there is a searchable record of what you learned, with no note-taking.
The lazy part: no habit to maintain and nothing to write down. You work and it records.
Demo
Start a throwaway ActivityWatch server, run the wrapper, type a few commands, then exit:
# terminal 1
aw-server --testing
# terminal 2
python aw_watcher_terminal.py --testing
# ...type a few commands, then exit...
# terminal 3: read the events back
curl -s "localhost:5666/api/0/buckets/aw-watcher-terminal_$(hostname)/events" | jq -c '.[] | {timestamp, duration, data}'
--testing must match how the server was started: aw-server --testing listens on port 5666 with a separate database, while a plain aw-server uses port 5600 and your real data. If they don't match, the watcher tells you which port it couldn't reach.
A real event from my machine, for the command ls:
{
"timestamp": "2026-10-26T06:53:00.822000+00:00",
"duration": 0.004932,
"data": {
"cmd": "ls",
"approx": false,
"output_bytes": 149,
"output_head": "aw_watcher_terminal.py LICENSE requirements.txt\nlaissez-log-post.md README.md typescript\n"
}
}
Code
The whole watcher is one file, aw_watcher_terminal.py, built on aw-client. To run it:
pip install aw-client aw-server
aw-server --testing &
python aw_watcher_terminal.py --testing
Options: --max-output N (bytes of output kept per command, 0 to disable), --settle SECONDS, --shell PATH, and --debug FILE to log terminal state without logging keystrokes.
How I Built It
Learning the system first
Before writing anything, I ran aw-server, aw-watcher-afk and aw-watcher-window as separate processes instead of using the bundled tray launcher, and poked at the REST API with curl. Three things became clear:
-
Everything is events inside buckets. A bucket is a named stream from one source on one host, and an event is
{timestamp, duration, data}with arbitrary JSON data. The server doesn't know what the data means; meaning is applied at query time. -
Watchers send heartbeats, not one event per poll. If a heartbeat has the same data and arrives within
pulsetimeof the last event's end, the server extends that event. Otherwise it starts a new one. My AFK bucket showed this directly: a singlenot-afkevent whosedurationgrew in steps of about 5 seconds (the watcher's poll interval) while itstimestampnever changed. -
I confirmed the rule by hand. From IPython, I created my own bucket and sent heartbeats with a 10-second
pulsetime. Two identical heartbeats a few milliseconds apart produced one event with a duration of 0.029s. Identical heartbeats more than 10 seconds apart produced separate events, and different data always produced a new event.
The watcher
There is no official watcher for terminal content. The closest one, aw-watcher-input, counts keypresses only and deliberately doesn't record which keys. So I built one:
- It runs your
$SHELLinside a PTY, relays bytes both ways, and rebuilds each command from the keystrokes you type. - It writes events with
client.insert_event. A command is a discrete event rather than a heartbeat stream, so it's written once the shell prompt has returned and output has gone quiet. - The window watcher keeps recording which window was active, so the two buckets can be joined by timestamp at query time.
Privacy decisions that shaped the design
A terminal recorder is sensitive by nature, so I made these choices on purpose:
- One session, not a global keylogger. A system-wide key hook would see every app. A PTY wrapper sees only the session it wraps.
-
Record only when the shell owns the terminal. Keystrokes count only while the shell is the foreground process of the PTY, which means password prompts (
sudo,ssh), editors, pagers and REPLs are never stored:
fg = os.tcgetpgrp(master) == pid # is the shell itself in the foreground?
recordable = fg and (echo_on or not canonical_mode)
-
Redaction and truncation.
password=,token=,api_key=patterns are redacted in both the command and its output, and output is cut to 500 bytes by default. - Everything stays local. The data goes into ActivityWatch's own SQLite database on your machine, where you can inspect it, change retention, or delete it.
Bugs I hit along the way
Each of these came out of reading real events and noticing something was off:
- My first password-prompt check looked at terminal echo flags and discarded every command, because readline turns echo off at a normal prompt too. The fix was to distinguish readline (echo off, canonical mode off) from password prompts (echo off, canonical mode on).
- Events were only written when the next command was entered or the shell exited. I switched to writing once the shell regains the foreground and the output settles. A subtle race here: the shell takes back the terminal slightly before it prints its prompt, so checking too early lost the output and recorded a 2-second
sleepas 0 seconds. - Pasting several lines leaked bracketed-paste codes (
[200~) into commands and split the paste into separate commands. I now parse escape sequences and treat a pasted newline as text. - Output carried the trailing prompt and the next command's echo. These are now trimmed.
AI use
The watcher has no model at runtime. I used an AI coding assistant (Claude) as a pair programmer while building and debugging it.
Limitations
- Commands are rebuilt from keystrokes, so history recall and tab completion can't be reconstructed. Those events get
approx: true. - Full-screen programs (like
info) paint with cursor movements rather than lines, so theiroutput_headis empty even thoughoutput_bytesis not. -
output_headrecords what programs print, which can include text you typed (a program that echoes its input will echo it into the log). Treat the database as sensitive. - Only the wrapped terminal is recorded, and only the first part of each command's output.
- I tested with scripted PTY sessions (echo, a silent
sleep, a multi-line paste, a password prompt andcat) and by hand on my own machine. Bash only; zsh and fish are untested.
Why Does Open Innovation Matter?
Because the thing I wanted is something a closed service wouldn't let me build.
- I could read how it works. Understanding heartbeat merging came from the open API and from experimenting against my own server, not from a vendor's description of a black box.
- I could extend it. ActivityWatch exposes an open REST API and a client library, so a watcher for a data source nobody had covered was an afternoon of work, not a feature request.
- The data stays mine. Command history and output are about as personal as data gets. With an open, local-first tool I can inspect the database, set my own retention and delete it. With a closed cloud tracker, I'd be trusting a company with my terminal.
- It leaves room for local AI. Because the log lives in an open local store, a locally run model could later summarize "what I learned today" without any of it leaving my machine. That isn't built yet, but it's the direction this opens.
The Lazy Payoff
I ran it on my own machine while building this post, and it recorded real sessions: ls, man pages, an info browser session and exit. The payoff I care about isn't a number of minutes saved. It's that I no longer have to remember to write down what I tried. The terminal history I'd normally lose is now an event stream I can query by day.
Top comments (0)