This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady

What I Built

A munshi is the clerk who keeps the register: spectacles, a cap, a ledger, and a quiet record of what happened today. Munshi is that clerk as a small buddy on my Mac's desktop.

At the end of every week I couldn't say where my hours went. My timesheet was guesswork, and I didn't want a cloud app watching my screen to fix that.

It saves three chores I am bad at: writing down what I did, rebuilding yesterday from memory, and guessing where my hours went. Anyone who keeps a log, bills by the hour, studies, writes or ships side projects gets to be lazier. So does anyone who sits down after lunch and wonders what they were doing before it.

The deal is that I do nothing. Every 10 seconds Munshi notes the front app and its window title. Every 5 minutes it reads the git branch, commit subjects and changed file names, only in folders I listed. Every 15 minutes Gemma 4 e4b, running in Ollama on the same Mac, turns each finished slot into blocks of work: a project, a topic and a short summary. When I click the munshi, one card answers four questions: where was I, what did I do today, what did I do yesterday, and how many hours went where. Copy for timesheet puts lines like tidepool · 2.5 h · tide-chart caching, CI fix on the clipboard. When a day ends, it writes a Markdown journal page and an hours CSV.

No screenshots, no keystrokes, no clipboard reading, no browser history, no file contents. Nothing leaves my Mac.

Demo

The munshi sits on the desktop at its real size, the munshi, and one click opens the card beside it:

The card window: where was I, today's blocks and the actions

The card, on the made-up demo data (the projects tidepool and lantern do not exist):

The Munshi card: where was I, today, yesterday and hours

Paused, with yesterday's hours:

The card while paused, showing yesterday's hours

Code

GitHub logo prateek11rai / munshi

A desktop buddy for your Mac that keeps your daily register with a local open-weight model (Gemma 4 via Ollama). No screenshots, no keystrokes, nothing leaves your Mac.

Munshi: your daily register, kept by a 4B model on your Mac

Munshi

Your daily register, kept by a 4B model on your Mac.

A small munshi (a clerk with spectacles, a cap and a ledger) sits on your Mac's desktop and keeps your register for you. It notes which app and window are in front and, for folders you choose, the git branch, commit subjects and changed file names. Every 15 minutes a local open-weight model (Gemma 4 e4b through Ollama) turns that into plain lines, and code checks every one. Click the munshi and one card tells you where you were, what you did today and yesterday, and your hours per project, ready to paste wherever you need them. Personal projects, study, writing, freelancing, open source: you never keep a log by hand or rebuild yesterday from memory.

Demo

The munshi sits on your desktop (actual size: the munshi). Click it and the card opens beside it.

The card window on a Mac: where was I, today's blocks and the actions

All…

The demo records nothing, so you can try it in a minute:

ollama pull gemma4:e4b git clone https://github.com/prateek11rai/munshi && cd munshi uv sync uv run munshi demo load uv run munshi start --demo 

How I Built It

It is one Python 3.13 project managed with uv: pywebview for the two windows (the 88-pixel buddy and the card), PyObjC for the macOS calls, SQLite for the store, and Ollama with gemma4:e4b for the words. Dependencies are pinned exactly, and uv's exclude-newer keeps new releases out until they have aged.

How Munshi works: collector, events, writer with Gemma 4 and code checks, blocks, notes, card, journal and hours, all inside the Mac

The collector reads the front app from NSWorkspace and the focused window's title through the Accessibility API. Three minutes without input starts an "away" stretch. Two details needed care. macOS only refreshes the front app while the main run loop runs, and pywebview owns the main thread, so the read is posted to the main thread and falls back to the window server's list when that is busy. And a terminal titled just "tmux" tells you nothing, so Munshi asks tmux for the active pane's command and repo name (never the path), only inside the folders I listed.

The writer takes each finished 15-minute slot, merges repeats, drops away time and gives every item a stable id (e1, e2, ...). It sends them to Gemma with the list of known projects and existing topics, asks for JSON under a schema at temperature 0, and gets back blocks of evidence, project, topic and summary.

Then code checks every block. The evidence ids must exist in that slot. The project must be one I have, and when code already detected the project from an editor title, a terminal or a git event, code wins. The topic is at most four words, must name a subject rather than an app, and must share a word with the evidence or the summary. The summary is at most 14 words and must use words that appear in the evidence. Digits are stripped from anything the model writes.

One writer run: compact the slot, prompt Gemma, check each block, accept or fall back, then the where-note

A block that fails, a model error or a timeout becomes a plain code-made line such as Preview · tide-tables-2026.pdf, so the timeline is never empty. Start, end and minutes always come from the evidence. Every number on the card comes from code: durations, hours, counts, "updated 6 min ago". The model only names things.

The privacy rules are code too, and they run before anything is written to disk:

Where private things are dropped: pause, own window, ignored apps, title words, process words, private windows, redaction, allowed roots, local-only model

I run it on the Mac I use for everything, so those gates are not theory. One chat app is on my ignore list. A word I never want in my register is a title word, so any window whose title contains it is never recorded, and repos, branches, commits or file names containing it are dropped. One of my two browser profiles is kept out by a process word: Munshi checks the browser's command line and, for Firefox, the profile folder it has open, so that profile stays out while the other one is recorded.

The last piece was measuring without looking. munshi stats and munshi eval --real print numbers only: blocks accepted, rejected by which check, fallback rate, seconds per slot, memory. The eval reruns a real day in a scratch copy of the store. The rule for the agent tuning Munshi on my real days was the same: numbers only, never a window title.

The tables are small and plain; the data model diagram shows how blocks cite events and notes cite blocks.

Why Does Open Innovation Matter?

A log of every window you open is the most private data on your computer. It holds who you talk to, what you read, what you are worried about and what you are paid to do.

Rewind, the Mac app that remembered everything, stopped capturing on 19 December 2025 after Meta bought its maker. The record it kept depended on a company, and the company changed hands. That is the risk whenever this kind of record lives on someone else's server or inside someone else's business.

With an open model on my own Mac, the record and the model both stay mine. I can read every line that decides what is recorded, and I can add the rules my situation needs, like a title word for something I never want recorded. No company can change the terms later.

I can also swap the model. Any Ollama model works: MUNSHI_MODEL=<name>, then munshi eval --demo --model <name> to compare. The checks are the same for every model, so a weaker model means more code-made lines, never wrong numbers.

My Agent Session

Munshi was built in two halves.

Session A is the base: collector, writer, card, journal, tests and docs, built in one evening with made-up demo data and fakes only. Real collection never ran in that session.

Munshi, session A: building a desktop buddy that keeps a local work journal (work laptop, demo data only)
You

Read ~/github/prateek11rai/munshi/docs/design/build-brief.md and do everything in it, in order. If anything is unclear, ask in this window and keep going on the parts that do not depend on the answer.

Agent
▸ Bash cat ~/github/prateek11rai/munshi/docs/design/build-brief.md; echo ---; ls -la...
$ cat ~/github/prateek11rai/munshi/docs/design/build-brief.md; echo ---; ls -la ~/github/prateek11rai/munshi ~/github/prateek11rai/munshi/docs ~/github/prateek11rai/munshi/docs/design ~/github/prateek11rai/munshi/.claude
# Build Munshi · base (this laptop, demo data and fakes) > The prompt the build agent gets. Lives at `docs/design/build-brief.md`; the planner appends answers at the bottom. Don't edit this file. Planner: tmux `mlh:0`. You: tmux `mlh:1`. Repo: `~/github/prateek11rai/munshi`, branch `main`. Challenge: https://dev.to/challenges/hacktoberfest-week3-2026-10-19, closes **Mon 2026-10-26 12:29 IST**. F …
Agent
▸ Bash cat ~/github/prateek11rai/munshi/docs/design/spec.md
$ cat ~/github/prateek11rai/munshi/docs/design/spec.md
# Munshi · spec Munshi is a small buddy that sits on your Mac's desktop and keeps your daily register. It quietly notes what you work on (which app, which window, which repo, which files), and a local open-weight model (Gemma 4 e4b through Ollama) turns that into a plain journal. Click the munshi and you get one card: **where you were**, **what you did today and yesterday**, and **your hours per …
Agent
▸ Bash echo "TMUX=$TMUX"; tmux display-message -p '#S:#I' 2>/dev/null; echo ---; cla...
$ echo "TMUX=$TMUX"; tmux display-message -p '#S:#I' 2>/dev/null; echo ---; claude mcp list 2>&1 | head -30; echo ---; ls -la .agents/skills/; echo ---; ls -la .claude; readlink .claude/skills; echo ---; ls ~/.claude/skills 2>/dev/null | grep -i devrelay; echo "home claude devrelay count: $(ls ~/.claude/skills 2>/dev/null | grep -c devrelay)"; ls ~/.agents/skills 2>/dev/null | grep -i devrelay; echo "home agents devrelay count: $(ls ~/.agents/skills 2>/dev/null | grep -c devrelay)"
(output omitted: lists the machine's other MCP servers)
Agent
▸ Bash echo "PANE=$TMUX_PANE"; tmux display-message -t "$TMUX_PANE" -p '#S:#I.#P' ; ...
$ echo "PANE=$TMUX_PANE"; tmux display-message -t "$TMUX_PANE" -p '#S:#I.#P' ; echo ---; claude mcp list 2>&1 | grep -i devrelay; echo ---; ls -la ~/.claude/skills | grep -i devrelay; echo ---; ls -la ~/.agents/skills | grep -i devrelay; echo; ls -la ~/.claude/ | grep skills; ls -la ~/.agents/ | grep skills
(output omitted: lists the machine's other MCP servers)
Agent
▸ Bash /bin/ls -la ~/.claude/skills/ | grep -i devrelay; echo ---; /bin/ls -la ~/.ag...
$ /bin/ls -la ~/.claude/skills/ | grep -i devrelay; echo ---; /bin/ls -la ~/.agents/skills/ | grep -i devrelay; echo ---; /bin/ls -ld ~/.claude/skills ~/.agents/skills; stat -f '%N %Sm' ~/.claude/skills/devrelay-challenges ~/.agents/skills/devrelay-challenges
12:drwxr-xr-x@ 3 owner staff 96 24 Oct 22:17 devrelay-challenges 13:drwxr-xr-x@ 3 owner staff 96 24 Oct 22:17 devrelay-community-wisdom 14:drwxr-xr-x@ 3 owner staff 96 24 Oct 22:17 devrelay-knowledge 15:drwxr-xr-x@ 3 owner staff 96 24 Oct 22:17 devrelay-mlh-submissions 16:drwxr-xr-x@ 3 owner staff 96 24 Oct 22:17 devrelay-navigator 17:drwxr-xr-x@ 3 owner staff 96 24 O …
Agent
▸ Bash git config user.email; bash .git/info/leak-check.sh; echo "exit=$?"; echo ---...
$ git config user.email; bash .git/info/leak-check.sh; echo "exit=$?"; echo ---; uv --version; /opt/homebrew/bin/python3.13 --version; ollama list; echo ---; cat .git/info/exclude; echo ---; /bin/ls -la .git/info/; git status --short; git log --oneline 2>&1 | head
165780365+prateek11rai@users.noreply.github.com leak-check: clean exit=0 --- uv 0.12.3 (Homebrew 2026-08-07 aarch64-apple-darwin) Python 3.13.12 NAME ID SIZE MODIFIED embeddinggemma:300m 85462619ee72 621 MB 13 days ago gemma4:e4b dc35e8d9c606 6.6 GB 2 weeks ago --- .agents/skills/devrelay-*/ .claude/settings.local.json .env …
Agent

Other Step 0 checks pass (noreply email, leak-check clean, uv 0.12.3, Python 3.13.12, gemma4:e4b present). Looking at what the leak check enforces, then starting the skeleton.

Agent
▸ Bash cat .git/info/leak-check.sh; echo ---; cat .claude/settings.local.json 2>/dev...
$ cat .git/info/leak-check.sh; echo ---; cat .claude/settings.local.json 2>/dev/null | head -50; /bin/ls -la .claude/; git config --list --show-origin | grep -i hook
#!/usr/bin/env bash # Local leak check for this repo (lives in .git/info, never committed). # Run before every commit (staged changes) and before every push (all history). # A check fails when it prints anything; output is cut to 160 chars per line. set -uo pipefail cd "$(git rev-parse --show-toplevel)" pat="$(git rev-parse --git-dir)/info/leak-patterns" want_email="165780365+prateek11rai@users.no …
Agent

Setup is fine apart from the DevRelay item above. Starting step 1, the skeleton: pyproject, uv config and a resolve to find the exact versions to pin.

Agent
▸ Bash mkdir -p ~/github/prateek11rai/munshi/src/munshi ~/github/prateek11rai/munshi...
$ mkdir -p ~/github/prateek11rai/munshi/src/munshi ~/github/prateek11rai/munshi/tests && cd ~/github/prateek11rai/munshi && printf '3.13\n' > .python-version && cat > pyproject.toml <<'EOF' [project] name = "munshi" version = "0.1.0" description = "A desktop buddy that keeps your daily work register with a local open-weight model." readme = "README.md" license = "MIT" license-files = ["LICENSE"] requires-python = ">=3.13" dependencies = [ "pydantic", "pywebview; sys_platform == 'darwin'", …
Using CPython 3.13.12 interpreter at: /opt/homebrew/opt/python@3.13/bin/python3.13 Resolved 24 packages in 1.03s name = "pydantic" version = "2.14.1" name = "pydantic-core" version = "2.50.1" name = "pyobjc-framework-applicationservices" version = "12.2.2" name = "pyobjc-framework-cocoa" version = "12.2.2" name = "pyobjc-framework-quartz" version = "12.2.2" name = "pytest" version = "9.1.1" name = …
Agent

Resolved within the 7-day cooldown: pydantic 2.14.1, pywebview 6.2.1, pyobjc 12.2.2, pytest 9.1.1, ruff 0.17.1. Pinning them exactly and syncing.

Agent
▸ Bash python3 - <<'EOF' import re,pathlib p=pathlib.Path("pyproject.toml"); s=p.rea...
$ python3 - <<'EOF' import re,pathlib p=pathlib.Path("pyproject.toml"); s=p.read_text() for a,b in [('"pydantic"','"pydantic==2.14.1"'),('"pywebview;','"pywebview==6.2.1;'),('"pyobjc-framework-Cocoa;','"pyobjc-framework-Cocoa==12.2.2;'),('"pyobjc-framework-Quartz;','"pyobjc-framework-Quartz==12.2.2;'),('"pyobjc-framework-ApplicationServices;','"pyobjc-framework-ApplicationServices==12.2.2;'),('dev = ["pytest", "ruff"]','dev = ["pytest==9.1.1", "ruff==0.17.1"]'),('"uv_build>=0.9,<0.13"','"uv_build= …
Using CPython 3.13.12 interpreter at: /opt/homebrew/opt/python@3.13/bin/python3.13 Resolved 24 packages in 13ms × Failed to build `munshi @ │ file://~/github/prateek11rai/munshi` ╰─▶ Expected a Python module at: src/munshi/__init__.py https://files.pythonhosted.org https://pypi.org 6:exclude-newer = "2026-10-17T00:00:00Z"
Agent
▸ Bash printf '__version__ = "0.1.0"\n' > src/munshi/__init__.py && printf 'def main...
$ printf '__version__ = "0.1.0"\n' > src/munshi/__init__.py && printf 'def main() -> int:\n """Run the munshi command line.\n\n Returns:\n The process exit code.\n """\n return 0\n' > src/munshi/cli.py && uv sync 2>&1 | tail -3 && W=.venv/lib/python3.13/site-packages/webview && ls $W $W/platforms && grep -n "def create_window" -A40 $W/__init__.py | head -80
 + ruff==0.17.1 + typing-extensions==4.16.0 + typing-inspection==0.4.4 .venv/lib/python3.13/site-packages/webview: __init__.py __pyinstaller _version.py dom errors.py event.py guilib.py http.py js lib localization.py menu.py models.py platforms py.typed screen.py state.py util.py window.py .venv/lib/python3.13/site-packages/webview/platforms: __init__.py android cef.py cocoa.py edgechromium.py …
Agent
▸ Bash cd ~/github/prateek11rai/munshi/.venv/lib/python3.13/site-packages/webview &&...
$ cd ~/github/prateek11rai/munshi/.venv/lib/python3.13/site-packages/webview && grep -n "def start" -A30 __init__.py | head -50; echo ----; grep -n "127.0.0.1\|localhost\|0.0.0.0" http.py | head; echo ----; grep -n "easy_drag\|mouseDown\|mouseDragged\|acceptsFirstMouse\|canBecomeKey\|ActivationPolicy\|loadFileURL\|file://" platforms/cocoa.py | head -40
164:def start( 165- func: Callable[..., None] | None = None, 166- args: Iterable[Any] | None = None, 167- localization: dict[str, str] = {}, 168- gui: GUIType | None = None, 169- debug: bool = False, 170- http_server: bool = False, 171- http_port: int | None = None, 172- user_agent: str | None = None, 173- private_mode: bool = True, 174- storage_path: str | None = Non …
Agent
▸ Bash W=~/github/prateek11rai/munshi/.venv/lib/python3.13/site-packages/webview; se...
$ W=~/github/prateek11rai/munshi/.venv/lib/python3.13/site-packages/webview; sed -n 50,95p $W/platforms/cocoa.py; echo ----; sed -n 440,510p $W/platforms/cocoa.py; echo ----; grep -n "def load_url\|loadRequest\|def _load_url\|resolve_url\|abspath" $W/platforms/cocoa.py $W/util.py $W/window.py | head -20
logger = logging.getLogger('pywebview') logger.debug('Using Cocoa') renderer = 'wkwebview' class BrowserView: instances = {} app = AppKit.NSApplication.sharedApplication() app.setActivationPolicy_(0) current_menu = None cascade_loc = Foundation.NSMakePoint(100.0, 0.0) _shared_app_delegate = None class AppDelegate(AppKit.NSObject): def applicationShouldTermi …
Agent
▸ Bash W=~/github/prateek11rai/munshi/.venv/lib/python3.13/site-packages/webview; se...
$ W=~/github/prateek11rai/munshi/.venv/lib/python3.13/site-packages/webview; sed -n 560,600p $W/window.py; echo ----; sed -n 100,175p $W/http.py; echo ----; grep -n "def move\|def show\|def hide\|def x\|def y\|events\.\|class Event\|moved\|def screens\|def evaluate_js" $W/window.py $W/__init__.py | head -40
 if self.events.loaded.is_set(): self.run_js(f'window.pywebview._createApi({func_list})') def _resolve_url(self, url: str) -> str | None: if is_app(url): return self._url_prefix if is_local_url(url) and self._url_prefix and self._common_path is not None: filename = os.path.relpath(url, self._common_path) return urljoin(sel …
Agent
▸ Bash W=~/github/prateek11rai/munshi/.venv/lib/python3.13/site-packages/webview; gr...
$ W=~/github/prateek11rai/munshi/.venv/lib/python3.13/site-packages/webview; grep -n "http_server\|is_local_url" $W/__init__.py $W/window.py $W/util.py | head -20; sed -n 875,890p $W/platforms/cocoa.py; sed -n 245,262p $W/window.py; sed -n 505,530p $W/__init__.py; grep -n "def move\|def show\|def hide\|def evaluate_js\|def run_js" $W/window.py
~/github/prateek11rai/munshi/.venv/lib/python3.13/site-packages/webview/__init__.py:33:from webview.util import _TOKEN, ImmutableDict, abspath, is_app, is_local_url ~/github/prateek11rai/munshi/.venv/lib/python3.13/site-packages/webview/__init__.py:141: 'http_server': False, ~/github/prateek11rai/munshi/.venv/lib/python3.13/site-packages/webview/__init__.py:170: http_server: bool = False …
Agent
▸ Bash cd ~/github/prateek11rai/munshi && cat > src/munshi/cli.py <<'EOF' import arg...
$ cd ~/github/prateek11rai/munshi && cat > src/munshi/cli.py <<'EOF' import argparse import sys def build_parser() -> argparse.ArgumentParser: """Build the argument parser for the munshi command. Returns: The configured parser. """ parser = argparse.ArgumentParser(prog="munshi", description="Your daily work register.") sub = parser.add_subparsers(dest="command", required=True) doctor = sub.add_parser("doctor", help="Check the setup.") doctor.add_argument("--d …
All checks passed! 6 files already formatted . [100%] 1 passed in 0.00s leak-check: clean
Agent
▸ Bash git add .gitignore .python-version LICENSE README.md pyproject.toml uv.lock s...
$ git add .gitignore .python-version LICENSE README.md pyproject.toml uv.lock src tests docs/design && git status --short && bash .git/info/leak-check.sh && git commit -q -m "Skeleton: uv project, doctor stub, design docs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>" && git log --oneline | head -1 && git status --short
ok 11 files changed, 830 insertions(+) A .gitignore A .python-version A LICENSE A README.md A docs/design/build-brief.md A docs/design/spec.md A pyproject.toml A src/munshi/__init__.py A src/munshi/cli.py A tests/test_cli.py A uv.lockleak-check: clean ok 66d7511 Skeleton: uv project, doctor stub, design docs
Agent

First commit is in (66d7511). Moving to step 2: config, the SQLite store, and redaction, with their tests.

Agent
▸ Write ~/github/prateek11rai/munshi/src/munshi/config.py (198 lines)
~/github/prateek11rai/munshi/src/munshi/config.py (198 lines)
File created successfully at: ~/github/prateek11rai/munshi/src/munshi/config.py (file state is current in your context — no need to Read it back)
Agent
▸ Write ~/github/prateek11rai/munshi/src/munshi/timefmt.py (212 lines)
~/github/prateek11rai/munshi/src/munshi/timefmt.py (212 lines)
File created successfully at: ~/github/prateek11rai/munshi/src/munshi/timefmt.py (file state is current in your context — no need to Read it back)
Agent
▸ Write ~/github/prateek11rai/munshi/src/munshi/store.py (501 lines)
~/github/prateek11rai/munshi/src/munshi/store.py (501 lines)
File created successfully at: ~/github/prateek11rai/munshi/src/munshi/store.py (file state is current in your context — no need to Read it back)
Agent
▸ Write ~/github/prateek11rai/munshi/src/munshi/redact.py (56 lines)
~/github/prateek11rai/munshi/src/munshi/redact.py (56 lines)
File created successfully at: ~/github/prateek11rai/munshi/src/munshi/redact.py (file state is current in your context — no need to Read it back)
Agent
▸ Bash cat > tests/test_redact.py <<'EOF' import pytest from munshi.redact import r...
$ cat > tests/test_redact.py <<'EOF' import pytest from munshi.redact import redact @pytest.mark.parametrize( ("raw", "want"), [ ("Mail from ana@example.org - Inbox", "Mail from [email] - Inbox"), ("https://github.com/tidepool/tidepool/pull/42?tab=files", "github.com"), ("Docs at http://user:pw@docs.example.com:8080/a/b", "Docs at docs.example.com"), ("see www.example.com/path?q=1 now", "see www.example.com now"), ("Fix #42 in tidepool", "Fix #42 …
Exit code 1 E501 Line too long (101 > 100) --> src/munshi/config.py:40:101 | 38 | mode = "all" # or "only": record just the apps listed in `only` 39 | only = [] 40 | ignore = [] # added to the built-in list (password managers, settings, lock screen) | ^ 41 …
Source:https://dev.to/prateek11rai/my-hours-now-write-themselves-every-15-minutes-from-a-4b-model-that-never-leaves-my-mac-o95

Related posts