A command line tool that uses the command history to implement a “punch clock” to record time worked.
I use three kinds of timesheets – paper, Toggl, and shell history. I’ve always used a combination of tools, because, for example, I forget to click the “off” button on Toggle. Preparing accurate timesheets is sometimes a process of finding all this history, and identifying the work intervals.
I added to this collection of methods by creating a “clock in” command with this prompt:
create a CLI command, “clock”, that helps to keep track of time worked. it works in tandem with the .zsh_history, which is the source of truth, because running the program will show up in the history. when user types “clock in”, the program checks .zsh_history, and reports the last work interval. work intervals are the time periods between “clock in” and “clock out” commands. when the user types “clock out”, it reports the length of the work interval that has now terminated. when the user types “clock” without an argument, it reports all the work intervals for the last 35 days.
The “clock” command produces reports that look like this:
β bin clock
work intervals, last 35 days
--------------------------------------------------------------------------
1. 2026-08-24 13:48:10 -> 2026-08-24 13:48:18 [8s]
2. 2026-08-24 13:49:34 -> 2026-08-24 13:50:46 [1m 12s]
3. 2026-08-24 13:50:56 -> 2026-08-24 13:51:02 [6s]
4. 2026-08-24 13:51:29 -> 2026-08-24 13:51:44 [15s]
--------------------------------------------------------------------------
total: 4 interval(s), 1m 41s worked
currently CLOCKED IN since 2026-08-24 13:53:24 (16m 02s so far)
Code language: CSS (css)
Here’s the command it produced
#!/usr/bin/env python3
"""clock β track work intervals using $ZDOTDIR/.zsh_history as the source of truth.
Usage:
clock in Record a punch-in (the command lands in history automatically).
Reports the last completed work interval for context.
clock out Close the open interval started by the most recent "clock in".
Reports its length.
clock Report all work intervals from the last 35 days, plus totals.
"""
import os
import re
import sys
from datetime import datetime, timedelta
HISTORY_FILE = os.path.expanduser(os.environ.get("HISTFILE", "~/.zsh_history"))
WINDOW_DAYS = 35
# Matches zsh extended-history lines: ": <epoch>:<duration>;command"
LINE_RE = re.compile(r"^: (\d+):\d+;(.*)$")
LOCAL_TZ = datetime.now().astimezone().tzinfo
def dt(ts):
return datetime.fromtimestamp(ts, tz=LOCAL_TZ)
def parse_history(path):
"""Return [(timestamp, command)] parsed from the history file."""
entries = []
try:
with open(path, encoding="utf-8", errors="replace") as fh:
for raw in fh:
m = LINE_RE.match(raw.rstrip("\n"))
if m:
ts = int(m.group(1))
cmd = m.group(2).strip()
# Skip continuation-line artifacts that carry no timestamp.
entries.append((ts, cmd))
except FileNotFoundError:
print(f"error: history file not found: {path}", file=sys.stderr)
sys.exit(1)
return entries
def is_clock(cmd, verb=None):
"""True if this history entry is a `clock [verb]` invocation."""
parts = cmd.split(None, 1)
if not parts or parts[0] != "clock":
return False
rest = parts[1].strip() if len(parts) > 1 else ""
if verb is None:
return rest == ""
return rest == verb
def find_punches(entries):
"""Pair clock punches from history into work intervals.
Returns (intervals, open_start) where intervals is a list of
(start_dt, end_dt) tuples oldest-first, and open_start is the
start time of an unclosed interval, or None.
"""
events = []
for ts, c in entries:
verb = None
if is_clock(c, "in"):
verb = "in"
elif is_clock(c, "out"):
verb = "out"
if verb is not None:
events.append((ts, verb))
events.sort(key=lambda ev: ev[0])
# Pair punches left-to-right: each "in" opens an interval, closed by the
# next "out". A stray "out" with nothing open is ignored; a stray "in"
# leaves the interval open.
#
# Note on the current invocation: zsh writes the running command's own
# history entry at varying times depending on how the shell was started,
# so we deliberately do NOT special-case it. For `clock in` the stale
# state is harmless (it just prints "already clocked in" once). For
# `clock out`, if the matching `clock in` hasn't been flushed yet, the
# close fails cleanly β run `clock out` again a moment later.
intervals = []
open_start = None
for ts, verb in events:
if verb == "in":
open_start = dt(ts)
else:
if open_start is not None:
intervals.append((open_start, dt(ts)))
open_start = None
return intervals, open_start
def fmt_duration(seconds):
seconds = int(round(seconds))
h, rem = divmod(abs(seconds), 3600)
m, s = divmod(rem, 60)
sign = "-" if seconds < 0 else ""
if h:
return f"{sign}{h}h {m:02d}m {s:02d}s"
if m:
return f"{sign}{m}m {s:02d}s"
return f"{sign}{s}s"
def fmt_stamp(dt):
return dt.strftime("%Y-%m-%d %H:%M:%S")
def report_intervals(intervals, title):
total = sum((e - s).total_seconds() for s, e in intervals)
print(title)
print("-" * 74)
if not intervals:
print("(no complete work intervals)")
for idx, (s, e) in enumerate(intervals, 1):
dur = (e - s).total_seconds()
print(f"{idx:>3}. {fmt_stamp(s)} -> {fmt_stamp(e)} [{fmt_duration(dur)}]")
if intervals:
print("-" * 74)
print(f"total: {len(intervals)} interval(s), {fmt_duration(total)} worked")
def main():
args = sys.argv[1:]
if len(args) > 1 or args and args[0] not in ("in", "out"):
print(__doc__.strip(), file=sys.stderr)
sys.exit(2)
now = datetime.now().astimezone()
entries = parse_history(HISTORY_FILE)
intervals, open_start = find_punches(entries)
if not args:
since = now - timedelta(days=WINDOW_DAYS)
recent = [(s, e) for s, e in intervals if s >= since]
report_intervals(recent, f"work intervals, last {WINDOW_DAYS} days")
if open_start:
elapsed = (now - open_start).total_seconds()
print(f"\ncurrently CLOCKED IN since {fmt_stamp(open_start)} ({fmt_duration(elapsed)} so far)")
elif recent:
print("\nstatus: clocked out")
return
if args[0] == "in":
if open_start:
print(f"already clocked in since {fmt_stamp(open_start)} "
f"({fmt_duration((now - open_start).total_seconds())} so far)")
print("use 'clock out' first if you meant to close that interval.")
sys.exit(1)
print(f"clocked in at {fmt_stamp(now)}")
if intervals:
ls, le = intervals[-1]
print(f"last interval: {fmt_stamp(ls)} -> {fmt_stamp(le)} "
f"[{fmt_duration((le - ls).total_seconds())}]")
return
# clock out
if not open_start:
print("not clocked in β nothing to close.", file=sys.stderr)
sys.exit(1)
closed = (now - open_start).total_seconds()
print(f"interval closed: {fmt_stamp(open_start)} -> {fmt_stamp(now)} "
f"[{fmt_duration(closed)}]")
if __name__ == "__main__":
main()
That single prompt produced the entire program, without any intervention on my part. (Getting to that prompt required a little research into .zsh_history logging, though.1) Watching it write the code was fascinating. It was testing use cases, testing how Zsh records history, adding useful messages.
I’ve only skimmed the code, and can’t say how good it is.
I wouldn’t have written it this way. I would have used grep.
Note
1. A long time ago, I created a text-based timesheet calculator. Then, I created a program called log_everything that would run on all my computers, and compile log files from command history, browser history, and other data sources. Writing a log importer for any existing log file was trivial. I think I was scanning mailbox files, too. I could get a comprehensive overview of personal activities from a single database. It was useful for seeing what work I’d done, but, it was creepy to spy on myself.