I work across a dozen projects at once: client work, side projects, coursework that grew legs. The hard part was never the code. It was remembering why I'd decided something three weeks ago, which project had gone quiet, and what I'd actually finished. Chat assistants didn't help. To find what I'd done on a project a week earlier, I'd dig up old sessions and scroll back through hours of conversation, and often still come away unsure, because the answer was buried in too much to read.
So I built Praxis. It keeps a log of my work as it happens: the decisions I make, what I worked on, and the commits behind it. I don't have to remember to log. Claude Code logs decisions while we work, and when I close the dashboard, Praxis reads the day's git commits and fills in any project I didn't log that day. It then updates each project's state from those logs, so when I come back to a project it already knows where I left off.
Claude Code session
logs decisions as we work
Me
praxis log '...'
Today's git commits
on dashboard close: session-digest fills in unlogged projects
↓ all write to
Daily log
one markdown file per day
↓ context-sync
STATE.md
per project: focus, threads, decisions
↺ next session starts with a project brief, back in Claude Code
Everything it knows lives in plain markdown files on my machine. There's a SQLite database too, but it only indexes those files so lookups are fast, and Praxis can rebuild it from the markdown at any time.
Three files, and strict rules about who writes them
Praxis keeps three kinds of file. The model never edits any of them freehand; each one changes only through code with clear rules:
- The daily log: one file per day, with my work and decisions grouped by project. New lines come from the
logcommand (run by me or by Claude Code) and fromsession-digest, which marks its lines(auto)and never touches lines I wrote. Nothing is ever rewritten; it only grows. STATE.mdin each repo: the project's current focus, open threads, next actions and recent decisions.context-syncrebuilds it from the logs and keeps my own## Notessection as it was. The only other thing that touches it isrepo-state-sync, which records the project's stack and last commit date at the top.TASKS.mdin each repo: tasks with ids, priority and the date they were done. Every change goes through one shared module, whether I'm closing a task or a new spec is filing ten of them.
The payoff is that when a project's state looks wrong, I know exactly which program wrote it and which log lines it came from.
Skills: small programs with one job
The actual work is done by 34 skills. Each one does a single job, like logging a line, closing a task, writing a standup or summarizing a day. They come in three kinds:
- Scripts: plain Python, no AI. Logging, editing tasks, indexing, setting up a new repo.
- Agents: Python that gathers the facts first, then hands them to the model to write up, usually in a single call. A standup, a project's current state, a summary of the day.
- Reasoning workflows: instructions Claude Code follows inside a session. Questioning me about a feature idea, writing the spec, then building it one task at a time.
If something can be computed, it's computed; the model only gets the parts that need judgment. Every model call goes into a usage log with the skill, the model and the token count, so I can see what each skill costs me.
The router and the read-only boundary
Most days I don't open Praxis at all. It works in the background, logging my work and keeping each project's state current, and that's most of its value. When I do open it, I sometimes ask it something, like "what did I do yesterday?". That question goes to ask. A small, fast model (Claude Haiku) picks up to three skills that can answer it, runs them, and writes the answer from what they return. The dashboard, voice and the terminal all go through this same path.
The skills ask can pick from are all read-only. The skill that edits tasks isn't on the list, and the code refuses to run anything that isn't, even if the model asks for it. So a question, typed or spoken, can't change anything. If the voice input mishears me, I get a wrong answer, not a wrong edit. Tests check this.
I had to loosen that rule a little when I added saved reports. Now a question can write files that are built from other files, like a daily brief or a record of what I asked that day, because those can always be rebuilt. It still can't touch tasks, logs or project state.
Routines instead of a scheduler
The upkeep happens at two moments: when I open the dashboard and when I close it. Opening it runs start, which finishes yesterday's daily brief, refreshes every project's state and shows me a standup. Closing the tab runs end, which turns the day's unlogged commits into log lines, rebuilds today's brief and syncs each project's state again. A refresh won't run them twice, and if one step fails the rest still run.
I tied them to the dashboard instead of a clock because my hours are irregular, and "when I start" and "when I stop" are better signals than 9am. The trade-off is that on a day I never open the dashboard, Claude Code still logs decisions as I work, but that day's commits never get turned into log lines.
What else it can do
Logging is the core, but the same files and connections make a few other things possible. They're there when I need them:
- The task board: every project's tasks on one kanban board. Dragging a card changes its status.
- Pull requests: what's waiting on me across my GitHub accounts, one section per account, each linking straight to the PR.
- Inbox: what needs a reply across my Gmail accounts, sorted into needs-reply, action and noise.
- The résumé tailor: I paste a job description and it drafts résumé bullets from work I've actually logged, each one backed by a quote from that log.
- Obsidian: reports like the daily brief are saved into an Obsidian vault. The dashboard shows an answer and then it's gone, which keeps the screen clear; if I want to reread it, I open it in Obsidian.
All of these sit on one shared core, and a later article will walk through that architecture in more detail.
What files cost me
Keeping everything in plain files has costs. Files have no transactions, so when two processes started writing the same report at once, I had to add a file lock and write to a temp file before swapping it in. And because STATE.md is rebuilt from the logs, I can't fix it by hand: if a line is wrong, I fix the log it came from and let it regenerate.
It's still the right trade for me. Everything Praxis knows, I can open and read.
What I'd tell someone building one
If you're building something like this, start with where the files live and which program is allowed to change each one, not with the AI. The model calls turned out to be the easy part. What makes me trust Praxis is that I always know where a fact came from, and anything that isn't one of those files can be rebuilt.
Next in this series: Lessons from three months of building my personal AI OS.