Le Laissez-Log: Record what you learn everyday, Lazily in the Background
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
<!-- TODO: add a screen recording or GIF: start aw-server --testing, run the wrapper, type a few commands, exit, then run the curl command below. -->
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
<!-- TODO: replace <you> with your GitHub username, then remove these comment markers:
{% embed github.com<you>/le-laissez-log %}
-->
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 pulsetime of the last event's end, the server extends that event. Otherwise it starts a new one. My AFK bucket showed this directly: a single not-afk event whose duration grew in steps of about 5 seconds (the watcher's poll interval) while its timestamp never 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 $SHELL inside 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:
1. 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).
2. 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 sleep as 0 seconds.
3. 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.
4. 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 their output_head is empty even though output_bytes is not.
- output_head records 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 and cat) 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.
Mentions
Le Laissez-Log: Record what you learn everyday, Lazily in the Background 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 <!-- TODO: add a screen recording or GIF: start aw-server --testing, run the wrapper, type a few commands, exit, then run the curl command below. --> 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 <!-- TODO: replace <you> with your GitHub username, then remove these comment markers: {% embed github.com<you>/le-laissez-log %} --> 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 pulsetime of the last event's end, the server extends that event. Otherwise it starts a new one. My AFK bucket showed this directly: a single not-afk event whose duration grew in steps of about 5 seconds (the watcher's poll interval) while its timestamp never 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 $SHELL inside 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: 1. 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). 2. 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 sleep as 0 seconds. 3. 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. 4. 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 their output_head is empty even though output_bytes is not. - output_head records 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 and cat) 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.
一天時間花去哪?ActivityWatch 回顧電腦使用|GitHub 開源快報 #Shorts ActivityWatch 記錄目前應用程式、視窗標題與離開鍵盤的時間,提供活動圖表與時間軸;瀏覽器活動需相應 watcher/擴充。開著視窗不等於專注或工作成效。官方隱私政策:預設使用資料存本機;v0.14 可選第三方 AI 摘要會把應用名稱、網域、分類、設備名稱和時間合計送給選定提供者,並非所有模式都不傳資料;分享前清除敏感內容。本片不操作或收集他人資料。GitHub API MPL-2.0。官方:activitywatch.net 專案:ActivityWatch/activitywatch 資料核對:2026-10-13 GitHub API 與專案 README。星數為當日快照。 畫面含 AI 生成的情境示意圖;GitHub 頁面為實際截圖。旁白為 AI 語音。 訂閱「辰星說Github」,每天兩支,用白話認識開源工具。 #Shorts #GitHub #開源
@basedlayer Sure, if you open source on it i might contribute to it. it could even take some elements of Activity Watch like nav bar icon (which could be better). github.com/ActivityWatch it does recognize the browser page as well.
每天在电脑上花了多少时间? ActivityWatch 自动记录应用和网页使用时长,在本地生成时间分布与活动分类。适合想了解工作习惯、减少分心的人。 ActivityWatch/activitywatch #时间管理 #隐私保护