tankslab.com/logbook/source

README.md

527 lines, exactly as they ship in the download.

# Friday Logbook Cross-session memory for Claude Code. It remembers what you worked on so you can ask "did we already fix this?" six weeks later and get an answer. It runs entirely on your machine. **It never contacts any server. Not ours, not anyone's.** There is no account, no signup, no licence key, no update check, and no usage ping. You do not have to take that on faith. Run this in the folder you installed it into: ``` grep -rE "https?://" . ``` It returns **nothing at all**. There is no network code in the package to find, and no URL in it either — not even in a comment or a test fixture, because a package whose own verification command produces hits teaches you to ignore the command. On Windows: `Select-String -Path *.py -Pattern "https?://"` — same result, no matches. --- ## Check it yourself: rebuild it and compare You do not have to trust that this tarball contains the source we published. Prove it: ``` python3 build.py ``` That rebuilds the archive from the files around it and prints a sha256. **If it matches the checksum published beside the download, the file you have is exactly this source.** Nothing else needs to be believed. It works because the build pins everything that normally makes two builds differ — file order, timestamps, owner, permissions, and the timestamp gzip hides in its own header. The date is a fixed release date, deliberately not the moment of building: a build stamped with its own build time could never be reproduced by anyone, and the feature would be theatre. The build script is **inside the package**, which is the point — a build script only we possessed would prove nothing. It needs `python3` and nothing else. No downloads, no package installs, no toolchain. A build script that fetched something would put a URL right back into a package that promises it has none. ### Run the tests too ``` for t in tests/*.py; do python3 "$t" || echo "FAILED: $t"; done ``` Plain `python3` — no pytest, nothing to install. Each one checks its subject by **breaking it first**, so a test that could never fail is not counted as a passing one. `tests/test_public_clean.py` is the one that enforces the promise at the top of this page, and it checks exactly the files `build.py` packages rather than a list someone maintained by hand. If you run the tests before rebuilding, note that a test runner may leave a cache directory behind. `build.py` ignores those, so the checksum is the same either way — that is deliberate, and it is why the exclusion covers whole directories and not just files. --- ## Why this exists The tool this replaces did all of the following, and its users were not told: - sent analytics to a third party keyed to a stable install ID, carrying a machine fingerprint and how much you work — its own consent field was never filled in - updated itself from the internet, unreviewed, while having sight of everything - hooked `PostToolUse` with matcher `*`, so **every tool result** crossed it — file contents, command output, whatever your tools returned - kept credentials in plain text in its database: bot tokens, an API key, and credentials embedded in URLs None of that was necessary to remember things. This is the same feature without any of it. --- ## Install Requires Python 3.10+. Nothing else — no pip install, no dependencies. ``` python3 install.py # macOS / Linux python install.py # Windows ``` It checks your Python has SQLite FTS5, runs the redaction selftest and **refuses to install if it fails**, creates the store, and wires exactly four hooks — SessionStart, Stop, PreCompact and SessionEnd, and nothing else. The table further down says which hooks it deliberately refuses to use and what each of them would have exposed. If you are running it from a folder you are about to delete — a temp directory you extracted the tarball into, for instance — it will create the store but **not** wire the hooks, and say so. Pointing live hooks at a path that disappears leaves every future session capturing nothing, silently. Pass `--force-hooks` if you meant it. **On Windows, the hooks are wired without a shell.** This matters more than it sounds. A hook written as a single command string is handed to a shell — bash on Linux and macOS, but PowerShell on Windows if Git Bash is not installed, and PowerShell does not execute a quoted path sitting at the start of a line. It parses it as text and stops: PS> "C:\Windows\System32\whoami.exe" "arg1" Unexpected token '"arg1"' in expression or statement. An earlier version of this installer wrote that form, which meant every hook it installed did nothing whatsoever on Windows without Git Bash — no capture, no handoff, no help at compaction — while looking completely healthy in `settings.json`. It now writes the interpreter and each argument as separate values, so no shell parses anything and spaces in paths stop mattering, which on Windows is the ordinary case rather than an edge case. `tests/test_install_wiring.py` fails if that ever regresses, and it runs on every platform so a change made on Linux cannot quietly break Windows. ## Use ``` logbook where you are, and what to type next logbook search "vault recovery" find past work; shows why each result matched logbook timeline --days 7 what happened recently logbook show 1234 full detail for one entry logbook digest "mail migration" the story of a subject, assembled from entries logbook health is capture actually working logbook signal the last failure report, if something broke logbook import bring an existing history in logbook wipe delete everything (see below) ``` ### Searching the way you actually think Type the question, not the keywords. If no entry contains every word you typed, Logbook widens the search a step at a time — your words as prefixes, then only the meaningful ones — and **tells you which reading it used**: ``` $ logbook search "why did the cron timer stop firing" (no row had all of your words - showing matches on the meaningful words) 1 2026-08-20 10:00 ● Fixed the cron timer that stopped firing on the build box ``` It stops at the first reading that returns anything, so the narrowest useful answer wins and you are never handed a wall of loose matches. It never widens silently: if the line above is missing, every word you typed was found. Results cover your entries, your **session summaries**, and — with `--prompts` — the things you typed. Ids starting `s` are summaries and `p` are prompts, so `logbook show s12` opens the summary rather than a different row that happens to share the number. ``` logbook search "backup" --project api --since 30 ``` `--project` and `--since DAYS` narrow it; `-q` drops the match evidence when you only want titles. ### Bringing your history in **You do not need any other tool for this.** Claude Code already writes every session to `~/.claude/projects` on your own disk. That is the source. If you have used Claude Code at all, your history is already there, and Logbook can read it. ``` logbook import ``` That looks in the usual places, shows you exactly what it found, imports it, proves what landed, and only then asks about anything left over. If you happen to have been running another memory tool, its database is simply one more place to look, and `logbook import` will offer to bring that across too. It reads that file and never writes to it. It is a source, not the point. **You do not have to choose between your privacy and your history.** Everything imported crosses the redactor first. That matters most here: tools of this kind have been found holding credentials in clear text, and importing blind would carry them straight into the new store. The import prints how many it removed. Importing twice is safe — it will not duplicate anything. ``` logbook import --source transcripts only the transcripts logbook import --path <file> a database somewhere unusual logbook import --no-cleanup never mention leftovers logbook import --keep-all import, keep everything, ask nothing ``` ### What the old tool leaves behind Uninstalling a memory tool does not remove what it harvested. **The tool is gone. Its copy of your work is not** — often hundreds of megabytes of it, including, in at least one measured case, credentials sitting in clear text. So after a verified import, `logbook import` will tell you what is still there and what is inside it: ``` This database holds 1,204 sessions and 25 passwords or keys in plain text. 9 chat tokens 1 API keys 13 web addresses with logins in them It has been on your disk since 12 July and is 203.4 MB. ``` It counts the kinds. **It never shows you a secret** — a tool that printed the credentials it found in order to warn you about them would deserve the joke. Then it offers, one item at a time, to move them aside. The rules it follows: - **Never in bulk.** One thing at a time, each shown before it is mentioned. There is no "select all". - **The default is keep.** Enter keeps. You have to type `MOVE` to move something. - **It moves, it does not delete.** Things go to a dated folder in your home directory, and it prints the exact commands to put them back. Deleting for good is something you can do yourself once you are satisfied. - **Only what it can positively identify.** No wildcards, no guessing from folder names. If it cannot say what a file is, it does not mention it. - **Nothing is offered until the import is verified** — rows counted and read back out of the new store. "Import, delete, then find out the import failed" is a sequence that cannot happen. - **Your Claude Code transcripts are never touched.** They are not ours and not the old tool's, and they are what everything can be rebuilt from. Each of those rules has a test that fails if the rule is removed (`tests/test_leftovers.py`). This is the most dangerous thing the package does, and "we were careful" is not evidence. --- ## Skipping compaction, and why you would want to When a conversation fills the context window, the usual fix is compaction: the model reads back the whole conversation and writes a summary of it. You pay for that reading. On a 200,000-token window that is roughly a 200,000-token bill to produce a few hundred lines, and it happens again every time the window fills. That cost lands hardest on the people with the least room to absorb it. A larger plan compacts less often and absorbs it more easily. A smaller one hits the wall sooner, pays the same toll each time, and gets less work done per unit of spend. The transcript is already a file on your disk. Pulling the important parts out of it is text processing, not reasoning, and text processing does not require a model. logbook handoff write # summarise this session locally logbook handoff review # see what it would carry, without consuming it # then close it and open a new one in the same folder The next session starts clean and picks the summary up automatically, once. Measured on a real 6.1 MB session on the machine this was written on: source 6,131,534 bytes (~1.5M tokens) brief 24,044 bytes (~6k tokens) time 0.2 seconds, no model call, no network reduction 252x ### The default: the agent writes its own handoff (a baton) A machine summary is a program deciding what matters. It is good at what you typed and what failed; it cannot know which file path, which half-finished step or which "this is only prepared, not running yet" the work actually hinges on. The agent doing the work knows - so by default it writes the handoff itself, while it still has the whole picture. It works from the end of each turn, through the `Stop` hook the tool already uses, reading only the size of the live context from the transcript (no new hook, nothing new seen). Against the size of your window: | at | what happens | |---|---| | 55% | the agent is asked to write a baton at this natural break | | 70% | asked again, firmly: write it before any more work | | 82% | the turn cannot end until a baton is on file - compaction is close and would choose for it | These are fractions, so they mean the same on a 200k window and a 1M one. Change them with `LOGBOOK_BATON_ASK`, `LOGBOOK_BATON_PUSH`, `LOGBOOK_BATON_HARD`; set the window with `LOGBOOK_WINDOW` if it is not detected. logbook baton status # how full the context is, which tier, what is waiting logbook baton write # what the agent runs, with its handoff on stdin Once a baton is on file the agent tells you it is ready. You type `/clear`, and the next context of the same session opens with the baton - the agent's own words first, then, underneath and clearly marked, what the machine attached: the instructions still binding and everything you asked, exactly as typed. It is delivered once, and only to the process that wrote it (or, for a brand-new session, only when the writer has gone and it is the only one waiting). If no baton was written, the machine handoff is delivered exactly as before. ### Three modes - pick the one you like logbook handoff mode baton # default logbook handoff mode seed logbook handoff mode pointer **baton** is described above. In the other two modes nothing asks the agent to write anything, and only the machine brief is carried: **seed** puts the whole brief into the new session up front. Nothing to fetch, nothing to forget. Measured at about 3,500 tokens on a long session. **pointer** puts in a short notice instead - your standing instructions, and the fact that a full brief exists with the command to read it. Measured at about 450 tokens, 87% smaller. The rest is fetched with `logbook handoff show` only if it is actually wanted. Your standing instructions ride along in **both** modes. A rule you are still bound by is not detail to be fetched on demand; if it only shows up when someone remembers to ask for it, sooner or later it will not show up at all. ### What it keeps, in priority order Sections are cut by **what they are worth**, not by where they sit in the document. The first version protected the top of the file and trimmed the bottom, which sounds sensible and is wrong: it threw away carried-over history first while a list of 500 command shapes sat safely near the top. 1. **Standing instructions** - the ones still binding on you. Never cut, in any mode. 2. **Every instruction you gave, word for word.** About 0.6% of the bytes and all of the intent. Never summarised, never trimmed. 3. **Carried-over history** from an earlier compaction. If a session was compacted before the handoff, that block is the only surviving record of its first half. 4. **What was left open** at the end, flagged to verify rather than trusted. 5. **Errors, and what the output actually said.** That is where the lessons are. 6. **Conclusions** - what was decided and reported finished. 7. **Files touched, and commands collapsed by shape**, so forty near-identical runs become one line with a count. ### Instructions typed while it was working A message you type while the assistant is busy is stored as a **different kind of record** from a normal message, with the text in a different place. Anything reading only the normal messages misses it entirely. That is not a rare edge case. In the transcript this was built against, 110 such records held several standing orders that appeared nowhere else in the file - including which operating systems had to be supported. If you work by queueing instructions as they occur to you, that is most of what you said, and an earlier version of this tool dropped all of it. It is captured now, and `tests/test_handoff.py` checks it stays captured. ### What it drops, and why that is safe - **Duplicate tool output.** Each tool result is stored twice in the transcript. One copy is enough. That was 627,022 bytes in the sample above. - **Thinking blocks.** Worth being precise, because the first measurement here was wrong: a saved transcript does not contain the reasoning text at all. In the sample, 411 thinking blocks held zero readable characters and 656,924 bytes of cryptographic signature. Dropping them removes an opaque blob worth about 10.9% of the file and loses nothing anyone could ever read. - **Successful tool output.** Kept only when the command failed. Anything dropped to fit a budget is **counted and printed** under `NOT INCLUDED`. A summary that quietly loses half your session while looking complete is worse than none. ### Look before you trust it logbook handoff review Prints exactly what would be carried forward, and what it would cost, without consuming anything. The whole saving here comes from trusting a local extract instead of paying a model to re-read your transcript, so the extract has to be something you can check cheaply, at any moment, before you rely on it. A summary you cannot inspect is just a shorter thing to be wrong about. ### It keeps one ready without being asked If a session is killed, crashes, or you just close the terminal, nothing gets the chance to write a handoff - and that is exactly the day you wanted one. So once a conversation is large enough to be worth carrying, Logbook quietly refreshes a current handoff in the background as you work, at most once every ten minutes. It is local text processing: about a fifth of a second, no model call, nothing leaves the machine. ### If you would rather keep compacting You do not have to change how you work. Compaction is still right when a session has to stay open. In that case the `PreCompact` hook passes the summariser a short steer naming what has already been extracted and repeating your standing instructions verbatim, and the full local extract is handed to the session that resumes on the other side of the compaction. So the model re-derives less from raw tool traffic either way. If that hook fails for any reason it stays silent and compaction proceeds exactly as it always would - a broken optimiser must never break your session. ⚠️ Worth knowing, because it was wrong here for days: `PreCompact` does not accept the JSON envelope the other hooks use. It reads the hook's plain standard output and nothing else, and it echoes that output to your terminal. The first version of this emitted the JSON shape, had every byte of it rejected by the harness, exited 0, and logged nothing - a perfectly computed summary thrown away on every single compaction, with no symptom visible from inside the tool. `tests/test_hook_contract.py` now asserts the shape the harness actually accepts, and `mutation_proof.py` puts the bug back to prove that test can still catch it. ### If more than one agent works in the same folder A handoff belongs to whoever continues that work, and when several sessions share a directory there is no way for a program to know which one that is. So it does not guess. One waiting handoff is delivered normally; two or more and it refuses, names them, and consumes none of them until you say which you meant: logbook handoff take --from <session> Handing a session somebody else's conversation is the worse failure of the two, and the quieter one: it reads as ordinary context and nothing about it announces that it belongs to someone else, while the session it was meant for starts empty and cannot tell that anything is missing. A handoff is also never offered back to the session that wrote it, and one older than eighteen hours is left on disk rather than injected as though it were current. ### These files hold your words A handoff is plain, readable text from your conversation, sitting in `handoff/` inside the data directory. `logbook wipe` deletes it along with everything else, and lists every file it is about to remove before it removes anything. That is checked by `tests/test_handoff.py`, which was written after the first version of `wipe` missed these files entirely and left the most readable copy of a session on disk. ## The three hooks it refuses, and why Claude Code lets a plugin hook many events. This uses **four**: `SessionStart`, `Stop`, `PreCompact` and `SessionEnd` - with two entries on `Stop`: capture, in the background, and the baton check, which reads only the size of the context and must answer before the turn ends. Every one of them reads the transcript Claude Code already wrote. It deliberately does **not** hook: | hook | what it would see | why we refuse it | |---|---|---| | `PostToolUse` matcher `*` | every tool result | file contents, command output, secrets your tools return — all of it, on every call | | `PreToolUse` matcher `Read` | every file you open | a record of everything you looked at, whether or not it mattered | | `UserPromptSubmit` | every prompt, as you send it | a subprocess on every keystroke-submit, and your raw input before anything filters it | If a memory plugin hooks those three, it can see effectively everything on your machine. That is worth knowing about any tool, including this one — which is why the hooks it installs are listed here rather than buried. --- ## What it stores, and what it removes first For each session: your prompts, what was done each turn, and which files were touched. Everything crosses a redactor **before** it is written. It removes private keys, API keys, bot tokens, JWTs, bearer and basic auth headers, credentials inside URLs, and `password =` style assignments. Every rule has a test proving it can still fire, and the install refuses to proceed if any of them stops matching — because a redactor that cannot fire reports "clean" forever. It is not magic. It is pattern matching, and pattern matching misses things. If you paste a secret into a prompt in a form nothing recognises, it can end up stored. The store is a plain SQLite file you can inspect, and `logbook wipe` removes all of it. --- ## Where your data lives | | | |---|---| | Windows | `%LOCALAPPDATA%\friday-logbook` | | Linux | `$XDG_DATA_HOME/friday-logbook`, else `~/.local/share/friday-logbook` | | macOS | `~/Library/Application Support/friday-logbook` | One directory. A SQLite file, and archives if you make them. Set `FRIDAY_LOGBOOK_ROOT` to put it somewhere else — an encrypted volume, a USB stick, wherever you prefer. Nothing is written anywhere else, and nothing is written outside your user account. ## Deleting it ``` logbook wipe shows every path and size, asks for confirmation logbook wipe --hooks also unwires it from Claude Code ``` It prints exactly what it will delete before deleting anything, and verifies the directory is empty afterwards rather than just claiming success. It does **not** touch `~/.claude/projects`. Those transcripts are Claude Code's and yours — Logbook only ever read them. Wiping Logbook removes what Logbook derived, not your own history. Use `--hooks`. Removing the store while leaving it wired in means it starts refilling tomorrow, and you would be right to call that a dark pattern. --- ## When something breaks Start with: ``` logbook health ``` It answers one question — *is this actually working* — with evidence rather than a green light, and every failure line says what to run next. It checks that the redactor can still fire, that both hooks are wired **by name** and point at a program that exists, that the write path still matches the schema, that the hook log holds no unread failures, and that **search can still find a row it is holding**. That last one exists because a search index can be dead while the store is perfectly intact: the row counts still look right, SQLite's own integrity check still passes, and every query quietly returns nothing. `--strict` also fails on warnings, for scripts. There is no crash reporting, because there is no network. Instead a failure mints a short code and writes a plain-text report **to your disk**: ``` logbook signal ``` The report holds what broke and what you are running on. Before it is written, it removes filesystem paths (which contain your username), names, and anything credential-shaped. Read it. If you want us to see it, you send it — by whatever means you choose. Nothing sends itself. Reporting nothing at all would be easier, but a tool that dies quietly on your machine and never tells anyone is not respecting you, it is failing you. ## Updating There is no auto-update, and there never will be. Updates are a thing a person chooses. Download a new version when you want one. ## Licence and cost Free. No account, no telemetry, no upsell.