chsum
Work logs and reload-ready context from your coding-agent conversations — Claude Code and Codex CLI, listed and digested side by side.
Digests and listings are never generated by a model. Every line of that output
is either copied verbatim from a transcript or computed from it, so nothing can be
invented. That matters because the output is designed to be pasted back into a
future Claude session, where a plausible-but-wrong sentence would become ground
truth. The one exception is labelled where it appears: chsum recap ends in
a timeline written by a model, printed beneath the verbatim record it was written
from.
Start from the problem you have:
- "What did we do yesterday?" or "Pick up where we left off." —
chsum digest --lastwrites a short file and prints its path: your prompts in order, each with the row it sits on, the commands that did something, and the last exchange, all copied or computed, no model.--stdoutprints it instead. →chsum digest - What did Claude do with what I said? —
chsum recapquotes each of your turns and accounts for the gap after it: files, commands, agents and failures computed from the transcript, then a model's bullets for that gap alone, printed beneath the record they describe. Oneclaude -pcall per turn, stored so a window is paid for once. →chsum recap - Claude is working in another terminal and you want to know what it has done so
far. — a bare
chsum recapreads the live transcript from your last prompt onward, failures quoted first. →chsum recap - A new session needs the context of an old one, without a model paraphrasing
it. —
chsum digest <ref> --stdoutprints the digest as a paste-ready artifact. →chsum digest - Which session was that, and which of them went anywhere? — bare
chsumlists this project's sessions with duration, prompts, files, agents and notes, dead ends included. →chsum - You remember what was said, not when. —
chsum find "…"searches the text of every conversation;--notessearches what you noted. →chsum find - chsum printed
01a0acf9:31and you want that record. —chsum where 01a0acf9:31prints thesedcommand for it, resolved to a real path. Pipe it toshto run it. →chsum where - You need the exact command Claude ran, or its full output. —
chsum digest <ref> --commandslists every Bash call in order;--call <id>prints one whole. →chsum digest - A subagent did the work and its report is buried. —
--agentsprints each report;<ref>/<agent-id>addresses the agent's own digest. →chsum digest - Something just worked and you want to find that moment later. —
! chsum note "…"files a note against the message you are on, or an earlier one by id or phrase. →chsum note - A session is titled after its first question, not what it became. —
chsum name "…"renames it, in chsum and in/resume. →chsum name - What did I work on this week, across every project? —
chsum --since 7d --all -n 0lists every session, under day headings. →chsum - You read transcripts in claude-history and want your notes there too. —
register
chsum annotationsas its annotator. →chsum annotations recap's files-touched section misses edits made outside Edit/Write. — opt in to git checkpoints; the plugin's hooks runchsum hook. →chsum hook
Coming from 2.x: what changed.
The commands, one section each:
| command | what it is for |
|---|---|
chsum |
list this project's sessions, newest activity first |
chsum recap |
what happened over a window of turns, with a model-written timeline |
chsum digest |
one conversation, verbatim and computed, plus row-level views |
chsum where |
a locator to the sed command that prints that row |
chsum checkpoints |
the git checkpoint chains this repo holds, and dropping old ones |
chsum find |
locate a conversation or a note by what it said |
chsum note |
mark the moment that mattered |
chsum name |
rename a session to what it actually was |
chsum annotations |
the wire claude-history calls to read and write notes |
chsum hook |
what Claude Code's hooks run, for per-turn git checkpoints |
The idea
Your own prompts already are a faithful record of what you were trying to do. Extracted in order they read as the story of the session — most of what a summary would have said, without the risk:
**m1**
> can you open the app in a browser, it is running on 3000
**m7**
> audio.worker.js:267 [Worker] Initialization failed…
**m137**
> when I change the playback rate the pitch shifts with it
**m153**
> The rate control in the toolbar no longer applies while it plays
Blockquoting is functional, not cosmetic: a quoted reply containing ## Summary
would otherwise forge a section of the digest. Everything else — dates, duration,
branch, files, commands — is parsed straight out of the transcript.
Install
As a Claude Code plugin, which brings the skill, the hooks and the code:
/plugin marketplace add InDate/indate-tools
/plugin install chsum@indate-tools
One installed tree runs everything. The hooks import the chsum.py sitting
beside them, so nothing has to be on PATH; a plugin places no command there,
and a hook naming one would fail at every tool call on a machine without it.
Where no chsum command resolves, the SessionStart hook names the file to run —
python3 <plugin>/chsum.py digest --last — so a session reaches the tool
without a second copy.
For a command of your own, symlink the plugin's file once:
ln -s ~/.claude/plugins/cache/indate-tools/chsum/<version>/chsum.py ~/.local/bin/chsum
Working on chsum itself takes pipx install --editable . from the checkout,
which puts that checkout on PATH and leaves the plugin's copy unused.
Requirements:
- Python 3.10+. No third-party packages, no model, no network.
claude-historyon yourPATH— forchsum findonly. Every other command reads the transcripts directly and spends no subprocess.claudeon yourPATH— forchsum recap's timeline only, which it writes throughclaude -p. Without it a recap still prints the verbatim record and a one-line notice.
Letting Claude run it
Inside a session, ! chsum … runs the command yourself. An agent has nobody
watching to approve a Bash call, so allow the command first, in
~/.claude/settings.json or a project's .claude/settings.local.json:
{ "permissions": { "allow": ["Bash(chsum:*)"] } }
The whole command, not just note: everything chsum does is read transcripts you
already have, and what it writes lands in its own store (see
Where chsum writes). Narrow it to Bash(chsum note:*) if
you'd rather approve the reading commands case by case.
The plugin can't set this for you — permissions come from settings files, and a plugin that allowlisted its own shell command would be granting itself something you never agreed to.
Picking a conversation, and a window of it
recap and digest take the same selectors, so a window you found in
one runs in the other unchanged:
chsum digest # the session you are in
chsum digest ch_3654a13c # a named one, by the ref the listing prints
chsum digest --last # the most recent session that isn't this one
chsum digest --last 2 # the one before that
chsum digest --file path/to/session.jsonl
chsum digest --messages 3 10 # your turns 3 to 10
chsum digest --messages -1 # your last turn, and everything after it
chsum digest --last 2 --messages -10 -1
A turn is one thing you typed — or one answer you picked from the question tool,
because a decision made by menu choice still steered the session — together with
everything that followed it up to your next turn. 1 is your first and -1 your
last; one number names one turn and two name a run of them, both ends included.
The pair resolves before it is ordered, so 2 -2 and -2 2 name the same window,
and an index past either end clamps. The header states what the numbers resolved
to — -10 -1 — your turns 45–54 of 54 — because a signed number alone says
nothing about where in the conversation it landed.
--last orders by last activity, so a session you resumed yesterday beats one you
started last week; run from inside Claude Code, the session doing the running is
excluded. Everything scopes to the current project; --all widens it.
A subagent has an address of its own, <parent-ref>/<agent-id>, taken by
digest:
chsum digest ch_da4e99d42e5efab11ebdedc22fb65145/a728cd49179f1a356 --stdout
chsum
Bare chsum lists the project's sessions, newest activity first:
Thu 06 Aug 2026 dur prompts files agents notes digest recap
ch_c120431a267b202aebf0b38f6c3c1b69 5h38m 78 14 - ⚑2 current 3/12 · 2h ago
↳ Plan the import pipeline from the sample files
Wed 05 Aug 2026
ch_da4e99d42e5efab11ebdedc22fb65145 3h03m 30 12 2 - stale -
↳ Set up the dev server
a43c4ff4401ca693e Quieten the test suite
a81d77b6cba4a46b3 Fix the retry backoff
ch_b99f11b7c257dafc8b93f53480ba3804 6s 1 0 - - - -
↳ (untitled)
chsum # the five most recent in this project
chsum -n 25 # more of them; 0 for all
chsum --since 7d # only the last week
chsum --all # across every project, fifteen of them, each row named
Listing is the default because picking is the common case, and "most recent" is often a session you abandoned after one prompt. Dead ends are listed, not hidden — that a session went nowhere is the answer to "where did that work go". The header counts them: no activity is no file, no notable command, no agent, and one prompt. The session running right now is listed too, tagged in progress; its numbers trail the conversation, since Claude Code is still appending.
Subagents are named, not just counted, because "3 agents" says nothing about a
session that delegated its work — and the id is the one chsum digest <ref>/<id> --stdout takes. Five deep, then a count. Five sessions too, by
default, and fifteen under --all, which spans every project and so lands you
in one of them at five: the listing is usually read into a context window, and
-n 0 --all is the whole corpus. --all adds a project column, since a row
otherwise names no project.
A digest column states whether a digest file covers the session — current,
stale where the transcript holds records written after the file, - where
there is none. A recap column states the turn store's coverage — 3/12 · 2h ago is turns holding a breakdown over turns there are — and a ✎ marks a title
you gave with chsum name rather than Claude Code's.
Sessions chsum's own summariser records are left out: recap calls claude -p
from the store directory, and Claude Code files each of those as a session of
its own. A run from inside that directory still lists them.
chsum recap
What Claude did with what you said. A digest gives your side of the conversation
and flat lists of files and commands; a recap takes a window of your turns and,
for each one, quotes it verbatim and then accounts for everything that happened
before you spoke again: the files touched, the commands run, the agents spawned,
the failures, all computed from the transcript, and beneath those a timeline
written by claude -p --model haiku from a verbatim extract of that one gap.
That timeline is the one section chsum prints that a model wrote. It is labelled
as such and printed beneath the record it was written from, so a wrong sentence
can be checked against what sits above it.
chsum recap # this session, since the last recap
chsum recap --last # the previous session, since its last recap
chsum recap --last 2 --messages -10 -1 # the last ten turns of the session before that
chsum recap ch_3654a13c --messages 3 10 # turns 3 to 10 of a specific session
chsum recap --full # the whole session (same as --messages 1 -1)
chsum recap --messages 3 10 --dry-run # what it would cost, no model call
chsum recap --messages 3 10 --no-cache # call for every chunk, store nothing
chsum recap --messages 3 10 --invalidate # summarise again, replacing what's stored
The bare run
chsum recap with no arguments is the session you are in, caught mid-run. Run it
from a second terminal while Claude works. It picks up from the turn after the
last one already recapped, anchors on the last thing you typed, then shows
everything since — files changed, commands run, agents at work, and what went
wrong. Failures get their own section, quoted from the actual error text, because
that is usually what you came back to find out. Like the verbatim sections the
timeline is a snapshot: the transcript trails the live screen.
Run from that second terminal there is no env var to say which session is running, so it lists the project's recent sessions and asks — newest at the bottom, next to the prompt, and Enter takes that one. Inside the session itself, or piped, there is no prompt to show: it picks.
What it costs, and what it stores
The timeline is written by several small, independent claude -p calls, not one
big one — the events are split into chunks at your own turn boundaries (by size
alone on the bare run) and each chunk gets its own call, run in parallel, none of
them seeing another's material or output.
--dry-run makes no model call: you get the verbatim record as usual, then a
breakdown of what each chunk would cost, by where its tokens come from (edits,
command output, what Claude said) and the largest single events. Character counts
are measured off the exact text that would be piped in; the token figures are
chars/4, hence the ~. It also states the fixed cost of the call itself —
claude -p sends its own system prompt and tool definitions unless told not to,
so chsum strips them (--tools "", --setting-sources "") and supplies its own
short instructions via --system-prompt, which brings that fixed cost down to
~158 tokens per call, counted once for every chunk that actually runs.
After a real run, the terminal gets one line of what it actually cost, summed across every chunk call — total input, cache reads and cache writes separately, output, elapsed, and how the estimate compared. Measured from the calls' own accounting, not guessed. It goes to stderr like the rest of the progress chatter, so a piped document is untouched:
haiku: 5,974 in (5,042 cache read) · 4,866 out · 56.0s · extract estimated ~3,211, harness ~2,763
Each turn's bullets are kept, so recapping a window twice costs nothing the
second time. The breakdown lands in the turn store (see
Where chsum writes) once that turn's gap has closed — a
later turn bounds it, or its last reply carries a stop_reason that ends the
exchange. Measured on one 3-turn window: 30.1s and two model calls, then 0.4s
and none, the same document both times.
Existing is not enough to reuse. A stored breakdown is read back only when it
names the same instructions, the same model, and the same events it was written
from; change any of the three and the turn is summarised again, with the old
breakdown kept beside the new one. --dry-run prices only what is missing, so
0 calls would be made means the window is already on disk. --no-cache neither
reads nor writes the store; --invalidate skips the read and still writes, so
the result replaces what was there for that window and nothing else.
The document
A recap spans several of your turns, so its timeline is sliced onto them: each turn quoted verbatim, and beneath it the bullets covering what happened before you spoke again.
### You said (21:43)
> ok
- **21:45** Created a new `is_typed_prompt()` helper that filters out
`<bash-…>` records, and updated five call sites to use it.
- **21:47** Tested the fix against a live session; chsum now reports
`prompts: 0` for the dead 2-second session and skips it.
### You said (21:50)
> commit the work
Placement isn't guessed: each turn's events are their own chunk (or chunks, if the gap was large), summarised by a call that sees only that turn's own material, so its bullets slot straight under the turn they follow — there is nothing to place after the fact, and nothing to get wrong by copying a time out of the extract. The verbatim sections above settle any disagreement between a bullet and what actually happened.
The per-turn files-touched bullets come from one of two sources, and the recap says which, counted:
## Files touched — 4 of 6 turns from a checkpoint
*Files for the other 2 turns come from the transcript scan, which sees
`Edit`/`Write`/`MultiEdit` only and carries each edit's line range as recorded
at the time.*
A git checkpoint (see chsum hook) sees every change however it
was made and its line ranges are current; the transcript scan sees neither. The
counts are measured — 0 of 6 says no checkpoint covered any turn, and does not
say why, because a hook that never ran and a reflog entry that aged out look the
same from here.
chsum digest
One conversation, verbatim and computed: your prompts in order, each with the
row it sits on; the files changed and the commands that did something; the last
exchange; and the sed line that opens any row. Every run writes a file and prints its
path — a bare one for the session you are in, a named one (a ref, --last,
--file) for that conversation — and --stdout prints the document instead.
One rule for every view below, not the digest alone.
https://github.com/user-attachments/assets/93f3cdac-cf82-403a-8333-5c8e42159fd7
chsum digest # this session, written
chsum digest <ch_ref> # that conversation
chsum digest <ch_ref> --stdout # print it instead of writing
chsum digest --file path/to/session.jsonl # address by file
chsum digest <ch_ref>/<agent-id> # one subagent's own digest
chsum digest --list # this project's digest files
chsum digest --list --all # every project's
--list reads the digest directory rather than a conversation, so it takes no
ref: one row per file, newest first, carrying the ref that reproduces it, when
it was written, the session's duration, prompts and files, and whether the file
is current or stale against the transcript. --all adds a project column
and the digests whose transcript is gone, which carry no project to scope by.
| Section | Source |
|---|---|
| Frontmatter — ref, title, project, branch, start, duration, counts | computed |
Notable — what you filed with chsum note, verbatim |
copied |
| What I asked for — your prompts, verbatim, in order, each under a line naming its row and what the turn did | copied, counts parsed from tool calls |
| Files changed / Commands run | parsed from tool calls |
| Delegated — one line per subagent, with its address | parsed from sidecars |
| Where I left off — last prompt and last reply, verbatim | copied |
| — found by two separate backward scans, so they may be far apart and are not a Q&A pair | |
Drill down — transcript and sidecar paths, and the sed that opens a row |
computed |
Each prompt leads, with its metadata beneath it — the row it sits on, how long the turn ran, and how many tool calls, files, commands and replies followed it:
> when I do --list on a note, it prints out the entire directory which looks terrible
*`49932ac7:37` · 19s · 2 commands · 1 reply*
The counts come from one walk of the transcript and its sidecars, so an agent's work counts toward the turn that launched it, and no model is asked anything.
An agent digest has the same shape minus the intent trail — an agent gets one
instruction, so Task is a single block. A subagent's edits and commands fold
into its parent's totals — otherwise a session that delegated everything reads as
no activity — and files no parent turn touched are marked (agent).
Every reference in a digest is a row: 1f271ca8:441 is line 441 of session
1f271ca8…'s transcript, and 1f271ca8/a190d601:87 is line 87 of that session's
a190d601… sidecar. Drill down expands both to full paths. Nothing needs a
second tool to resolve.
Output is budgeted, because it lands in a future context window: quotes clip,
lists cap. Every truncation is marked ([+N chars, sed the row below], …and N more) so you always know when you're seeing a fragment.
Commands run shows ten, and it shows them after a filter that drops
look-only commands (ls, cat, grep, and 27 more) and after a dedupe. Its
overflow line therefore carries two numbers and the invocation that opens the
rest:
- …and 25 more of these — `chsum digest ch_c77196cc… --commands` lists all 60 in order
The row views
Each writes a file of its own, <uuid>-<view>.md beside the digest, and prints
the path; --stdout prints the document instead. The same rule the digest
itself follows. chsum digest --list names the view beside the session each
file belongs to.
chsum digest <ch_ref> --messages # every message in order, whole
chsum digest <ch_ref> --tools # every tool call in order
chsum digest <ch_ref> --commands # every Bash call in order
chsum digest <ch_ref> --call <id> # one tool call whole, with its output
chsum digest <ch_ref> --agents # every subagent and what it reported back
chsum digest <ch_ref>/<agent-id> --commands # narrowed to that sidecar
chsum digest <ch_ref>/<agent-id> --messages # everything that agent said
chsum digest <ch_ref> --messages --stdout # print it rather than write it
chsum digest <ch_ref> --messages > out.md # or your own path
All three print in timestamp order across the transcript and its sidecars, with
nothing filtered, deduplicated or collapsed. --tools and --commands print one
line per row — the call id, a <session>:<line> locator, local time, the tool
name and the first line of the call. A row runs long and lets the terminal
soft-wrap it rather than folding at a space: a command broken across lines can
no longer be copied in one selection.
--messages prints its rows whole, on every ref. Messages are prose, and a
conversation clipped to a line each is the one thing this view cannot be used
for. There is no flag or size limit behind that: the turn numbers below already
select the part of a conversation you want, and a second way to ask for less
would only be a worse one.
Every view is broken by turn, each headed with the line the digest already prints under that prompt:
*turn 8 · 16m · 6 tool calls · 5 files · 108 commands · 12 replies*
The head names the turn a row sits in, not the row that opens one, so a view
that keeps no messages — --tools, --commands — still shows its boundaries,
and a window opening mid-conversation says which turn it landed in. --call <id>
prints one tool call and its captured output whole, which is where the text a
clipped call row actually lives.
--agents lists every subagent the session ran and each report it sent back,
numbered where an agent returned more than once. A report is the agent's own
document and runs to thousands of characters, so it is clipped with the cut
marked; the row it came from names where the whole text is.
One or two numbers beside --messages, --tools or --commands narrow the view
to a window of your turns and print those rows whole, so a stretch of
conversation reads without leaving chsum:
chsum digest --last --messages -1 # your last turn, and everything after it
chsum digest <ch_ref> --messages 10 11 # your 10th and 11th turns
chsum digest <ch_ref> --tools -5 -1 # what Claude called across your last five
chsum digest <ch_ref>/<agent-id> --tools 2 -1 # an agent's own turns, not yours
On an agent ref the turns counted are the sidecar's own: the task it was handed
is turn 1, and a prompt sent to it while it worked opens the next. Most agents
have exactly one turn, so the numbers earn their keep on a /btw fork that was
talked to repeatedly.
Inside a window the rows a view steps over are listed rather than skipped
silently, one line each with its own locator, so a jump from 2500 to 2513
reads as:
*⋯ 4 rows not in this view*
- `dc215bdd:2503` — thinking
- `dc215bdd:2504` — Bash, result 319 chars
- `dc215bdd:2508` — Bash, result 536 chars
- `dc215bdd:2512` — thinking
A tool call carries its tool and the size of what it returned, and its result row
folds into it. A thinking row is named and nothing more — the JSONL keeps its
signature and drops the text. Harness bookkeeping rows are left out.
A ## Sources block at the top expands every locator to a path, ~-relative so
it fits the width and still pastes into a shell. Where the view clipped its rows
it also gives a worked sed line, so a row reaches its raw record without chsum:
## Sources
- `f1b9bbc6` `~/.claude/projects/…/f1b9bbc6-….jsonl`
- `f1b9bbc6/a190d601` `~/.claude/projects/…/subagents/agent-a190d601….jsonl`
A row's whole text — `sed` the line its locator names:
- `sed -n '18p' ~/.claude/projects/…/f1b9bbc6-….jsonl | jq -r '…'`
- `01CPKjYaSD` `f1b9bbc6:26` 11:30:28 Bash `ls && wc -l chsum.py`
The sed line is an escape hatch for text a row clipped, so a view that prints
every row whole — --messages, or any turn window — leaves it out. One source prints as the path alone: there is nothing for a locator to pick
out, and the bare path is a line the terminal leaves intact to copy.
Upgrading to 3.0
Two changes break a script written against 2.x.
Every view writes a file and prints its path. In 2.x a bare chsum digest
and the row views printed their document to stdout; now they write it and stdout
carries the path alone, so chsum digest <ref> --messages > out.md puts a path
in that file rather than the messages. --stdout restores the old behaviour:
chsum digest <ref> --messages --stdout > out.md # the document, as 2.x gave it
code $(chsum digest <ref> --messages) # what the path is for
Checkpoints are kept until dropped. 2.x committed each turn and reset it
away, leaving it in HEAD's reflog to expire in about thirty days. 3.0 chains
them under refs/chsum/<session>, which git keeps — they survive a gc, the
reflog expiring, and the removal of the worktree they were written in, and they
no longer clear themselves. chsum checkpoints --prune 30d is the retention
that was previously accidental, and chsum checkpoints --migrate rebuilds
checkpoints written by 2.x so they gain the same durability. Both are described
under chsum hook.
Which tools' sessions it reads
Claude Code sessions, from ~/.claude/projects, and Codex CLI sessions, from
~/.codex/sessions. Both appear in every listing, digest and search. A source
column appears where the rows disagree and stays absent where one tool wrote
them all; chsum --source codex narrows a listing to one tool.
Claude Code writes the record shape chsum reads, so its transcripts are read
where they lie. A Codex rollout is written once into that shape under
<data dir>/chsum/translated/codex/, and rebuilt whenever the rollout grows or
the translation rule changes. Every line number a drill-down prints names that
translated file, so sed -n '1454p' <path> lands on the record the digest
quoted.
Three properties of a Codex rollout show up in the output, and each is the file speaking rather than a fault in the reading:
- No titles. Codex records no title for a thread, so a row reads
(untitled)untilchsum namegives it one. That name holds in chsum; it does not travel back to Codex, which never reads the translated copy. 0sdurations. A thread imported into Codex carries one timestamp on every record, stamped when the import landed. Duration is the spread of the stamps, so such a session reports0showever many turns it holds. Of 102 rollouts measured, 46 are imports.- No reasoning. Codex encrypts model reasoning into
encrypted_content, sealed server-side with no key on the machine. The plaintextsummarybeside it, present on roughly a third, becomes a thinking block. chsum skips thinking rows for both tools, so nothing printed changes.
Rollouts older than seven days are rewritten as .jsonl.zst by recent Codex
versions. Reading one needs a zstd decoder, and chsum holds no dependency
outside the standard library, so compressed rollouts are left alone.
Adding a third tool costs one file in chsum/sources/: where its sessions sit,
which directory each ran in, and a translation to the common record shape. No
reader below that layer names a format.
chsum where
Every locator chsum prints — 01a0acf9:31 beside a turn, f1b9bbc6:26 on a
row — names a line in a file. This turns one into the command that prints it:
$ chsum where 01a0acf9:31
sed -n '31p' /Users/joshua/.local/share/chsum/translated/codex/…/01a0acf9-….jsonl | jq
The command alone goes to stdout, so it pipes and substitutes:
chsum where 01a0acf9:31 | sh # print that record
vim $(chsum where 01a0acf9 | awk '{print $4}')
The file it resolved to, and where the line sits in it, go to stderr — beside the command on a terminal, out of the way in a pipe.
Four forms, all of them things chsum printed:
| you have | you type |
|---|---|
| one row | chsum where 01a0acf9:31 |
| a run of rows | chsum where 01a0acf9:31-40 |
| a subagent's row | chsum where 01a0acf9/a9f0f78b:3 |
| a session, no row yet | chsum where 01a0acf9 — prints the path and the template |
| a tool call id | chsum where toolu_01V7rDx5Le — resolves to the row it sits on |
| a checkpoint stamp | chsum where <session>:<call-id> — both halves, as the reflog holds them |
A ch_… ref works wherever a session id does. A line past the end of the file
stops and says how many rows the file holds, rather than handing back a sed
command that prints nothing.
A checkpoint commit is stamped chsum-checkpoint: <session> @ <time> <call-id>,
so a line read out of git reflog reaches the call that caused it:
git reflog --format='%gs' | grep chsum-checkpoint | head -1 |
sed 's/.*checkpoint: \([^ ]*\) @ [^ ]* \(.*\)/\1:\2/' |
xargs chsum where | sh
A bare id works too — chsum where toolu_01V7rDx5Le searches this project, then
every project. Naming the session beside it reads that one file directly.
chsum find
Locate a conversation, or a note, by what it said. This is the one command that
runs claude-history, for its embedding index.
chsum find "playback rate pitch shift" # locate a conversation
chsum find --notes "backoff" # notes whose text matches
chsum find --notes "backoff" --all # across every project
chsum find "ENOENT" --lexical # fast, for identifiers and error text
--hybrid (default) and --semantic are best for conceptual recall but are slow:
tens of seconds warm, and several minutes on the very first run while the
embedding index builds. Use --lexical (sub-second) for identifiers, filenames,
and error strings, or --exact for exact tokens. --all searches every project.
Each hit carries its score, the project, and the passage that matched, clipped
around the query term. Where the ranking could not run — an unbuilt embedding
index, a transcript with no searchable metadata — the run says so rather than
printing the hits alone: warning: semantic unavailable — …, and a line reading
8 hits ranked by position only (0.0147–0.0164), which means each score is a
reciprocal rank and the spread measures position, not how well a passage matched.
On a terminal the results are padded columns, coloured per project, with the
query term picked out in the excerpt. Everywhere else — a pipe, a capture, a
chat — the same results are markdown, so grep and a reading model get whole
records rather than clipped columns. Every listing follows that rule:
sessions, note --list, recap --list and digest --list print a table or a
bullet per row when stdout is not a terminal.
chsum note
Files a note against the moment you're in, so the digest says which part
mattered — extraction can tell you what changed, not which of it was the point.
chsum annotate and chsum mark are the same command.
! chsum note "the backoff approach, after two dead ends"
It prints nothing. The note goes into chsum's own store, filed under the turn of yours whose gap holds the message it points at, with the file, the row and the first line of that message stamped beside it. The transcript is never written to. Run outside a session it still files the note, against the newest transcript of this project.
Noting an earlier moment
List recent messages and name one:
! chsum note --recent 20
47dca7e9 06:27 you can we retry with backoff instead of failing
be74e21f 06:40 claude A fixed delay collides — every client retries in the same second
a27a1c9c 06:41 claude Edit: client.py
0b2f4db2 06:42 claude Bash: python3 -m pytest -x
! chsum note --at be74e21f "the thundering-herd point, explained properly"
Everything that happened, in order: both sides' messages and every tool call,
so you can note the edit or the command rather than the sentence near it. Tool
results are left out — a note resolves to the message containing the action
either way. Ids come from the transcript itself, and --at <line> takes a bare
row number too.
Or name the message by something it said:
! chsum note --match "worth knowing exactly where it dies" "the timeout gap"
Matching folds case, punctuation, and markdown away — currently no finds
Currently **no** —, because nobody retypes the asterisks. Notes still quote the
original bytes. If more than one message matches, chsum lists the candidates and
files nothing: asking to note a phrase puts that phrase in your own prompt too,
so "newest wins" would keep noting the request instead of its subject. chsum note's own calls are excluded from matching — its tool call is recorded before
the command runs, so otherwise every search would find itself.
Noting while an agent is working
! is not available while you are addressing an agent — what you type goes to the
agent as a message. Two things do work:
- Note it afterwards, from the session:
chsum note --match "<phrase the agent said>"searches the sidecars too, so the agent's own words are addressable. - Ask the agent to note it as it goes, with the command allowed as described under
Install. Its notes fold into the parent, like its
edits and commands, tagged
agent <id>with the row in that sidecar — sidecars' rows don't number against the parent's. Either side can delete the other's.
Where notes show up
As Notable at the top of the digest, verbatim, with the message they point at;
as a ⚑ count in the listing; in chsum find --notes <query>; and
in claude-history's viewer (see chsum annotations).
Listing, locating and deleting
! chsum note --list
id created
dde43c3c#1 2026-09-04 the timeout gap, stated plainly
↳ Currently **no** — and worth knowing exactly where it dies.
dde43c3c#2 2026-09-04 Testing
↳ Left in place — it records the state that prompted the change.
! chsum note --list --all # every project, with a project column
! chsum note --list --full # whole text, whole targeted message
! chsum recap --list # the bullets `recap` wrote, same ids
! chsum note --show dde43c3c#1 # where it landed, with what surrounds it
! chsum note --delete dde43c3c#2 # takes several ids at once
Every listing covers this project and --all widens it to every project. The
project column --all adds is painted a colour per project, so rows from one
project group by eye down a listing that crosses several.
The store holds two kinds and each has its own listing: chsum note --list is
your notes, chsum recap --list is the bullets recap wrote. A bullet is a
model's and a note is yours, and one listing holding both reads as one kind of
thing. An id is the turn's uuid and a number issued once — a bullet that recap
rewrites takes a fresh number, and a note keeps its own, so an id you copied out
still names the same text later. --show and --delete take an id of either
kind; deleting a recap bullet means the next recap of that turn prints without
it.
--show takes the same id and answers where: the file, the row in it, the time,
the agent when the message is a subagent's, then the targeted message whole and
--context N records either side (3 by default). It resolves across the
project, so an id from any session of it works.
Marks made before 2.2 were recorded as a marker line in the transcript.
Nothing reads those any more; the transcripts keep them, and claude-history
still finds their text as conversation.
chsum name
Sessions are titled by Claude Code, from the first thing you said — so a session
that started as one question and became a day's work is filed under the question.
chsum name fixes that:
chsum name "retry: design + build" # the session you're in
chsum name ch_3654a13c "retry: design + build" # one from last week
chsum name --list # renamed in this project
chsum name --list --all # renamed anywhere
chsum name --clear # back to Claude Code's title
chsum name --no-resume "…" # rename in chsum only
Your name wins everywhere chsum shows a title — listing, digest — and is
flagged ✎ in the listing, because whose reading of the session it is matters.
It also lands in /resume. Claude Code's title is an ai-title record it appends
to the transcript as the conversation grows, dozens per session, last one wins;
chsum name appends one more of exactly that shape. Never a rewrite of a line
already written — the one thing chsum adds to a transcript, and it is added the
way Claude Code adds it.
That is why the name is also kept in the store's names.json: rename a session
that is still running and Claude Code will title it again ten minutes later.
chsum keeps yours; /resume may drift back. --clear undoes a rename by
appending Claude Code's own title back as another record, so /resume reverts
too. --no-resume renames in chsum only and leaves the transcript alone.
chsum annotations
The wire claude-history calls when it
renders and searches annotations — not a command you type. Register chsum as an
annotator in ~/.config/claude-history/config.toml:
[annotations]
write_to = "chsum"
[annotators.chsum]
command = "chsum annotations"
Every note and every recap bullet then shows in its viewer at the row it points
at and matches in claude-history agent search, tagged by kind. A note typed in
its viewer (a) is filed by chsum the same way chsum note files one, and d
there deletes through chsum. chsum annotations read|write|delete takes one JSON
object on stdin and answers with one on stdout.
A note carries created and modified, RFC 3339, which the viewer prints beside
it. A write stores the stamps it is sent and takes chsum's clock where it is sent
none. A write carrying replaces supersedes the note that id names: the same id
comes back where the text changed under the same turn, and claude-history issues
no delete; a different id comes back where the new targets belong under another
turn, and claude-history drops the superseded note. delete answers
{"deleted": false} for an id the store does not hold — a non-zero exit would
drop chsum from the merge and take every other note on that transcript with it.
chsum hook
What Claude Code runs from the plugin's hooks/hooks.json — not a command you
type. It exists so recap's files-touched section can come from git rather than
from the transcript.
chsum hook stop # on every Stop: checkpoint the working tree
chsum hook session-start # on every SessionStart: raise the opt-in, once
The hooks are declared by the plugin, which installs globally, so they run in
every repository — and do nothing until that repository opts in. The gate is
.git/chsum-checkpoint, containing enabled or declined; it is per-checkout
and never committed. When the file is absent, the SessionStart hook asks Claude
to put the question to you once, and the answer is what writes the file.
Once enabled, each tool call that changes the tree is committed under
refs/chsum/<session>, chained onto the previous checkpoint and tagged
chsum-checkpoint: <session> @ <timestamp> <tool-call-id>. The commit is built
with git commit-tree from a throwaway index, so HEAD, your index and your
working tree are never written — nothing here can leave a checkpoint as your
branch tip, and your own commit hooks never fire. It shows in no normal git
command: git log, git branch and git status are unchanged. git log --all
does list it, since that means every ref.
recap and chsum digest --writes read the chain and diff consecutive
checkpoints, which sees every change however it was made and carries current
line ranges; the transcript scan sees Edit/Write/MultiEdit only.
Because each checkpoint is parented on the last, git show <checkpoint> is that
call's own diff and git log refs/chsum/<session> is the session's history. A
ref is a gc root, so a chain survives git gc, survives the reflog expiring,
and survives the removal of the worktree it was written in — all three of which
lost the old reflog-based checkpoints.
That durability is why retention is asked for rather than waited for:
chsum checkpoints # the chains this repo holds, and the gate
chsum checkpoints --enable # turn checkpointing on for this project
chsum checkpoints --disable # off; chains already recorded stay readable
chsum checkpoints --prune 30d # drop chains older than 30 days
chsum checkpoints --prune 30d --dry-run
The gate is per project and per worktree, held in the git directory. --enable
and --disable are both decisions, so either one stops the session-start prompt
asking again.
Dropping a chain leaves the transcript untouched and makes its commits
unreachable, which git reclaims on its next gc.
Checkpoints written by an older chsum sit in HEAD's reflog instead. They are
still read — the two sources merge on the stamp each checkpoint carries — but
they keep the old fragility: unreachable, so a gc takes them, and held in the
worktree's own reflog, so git worktree remove takes them with it. Rebuild them
as chains to keep them:
chsum checkpoints --migrate --dry-run # sessions still reflog-only, and counts
chsum checkpoints --migrate # rebuild each as a chain
A project still holding reflog-only checkpoints is told so once at session start, the same way the opt-in is raised, and the message names this command.
Each checkpoint is rebuilt from the tree and message it already carries, so the
content is identical and only the shas change — a parent is part of what a sha
hashes, so an existing commit cannot be re-parented. Nothing downstream resolves
a checkpoint by sha; --writes, recap and chsum where <call-id> all match on
the session and call id in the message. The reflog entries stay where they are,
and a second run finds nothing to do.
Where chsum writes
Digests, your session names and the turn store share one directory:
<data dir>/chsum/
digests/<uuid>.md `chsum digest <ref>`'s output (`--out` to change)
digests/<uuid>-<view>.md a row view's output — messages, tools, commands, agents, call-<id>
names.json your names for sessions
turns/<project>/<turn-uuid>.json one turn's recap bullets and notes
translated/<tool>/projects/<project>/<id>.jsonl another tool's session, in the record shape chsum reads
CHSUM_DIR in the environment names the directory outright. Otherwise
XDG_DATA_HOME set puts it at $XDG_DATA_HOME/chsum; on Windows it is
%LOCALAPPDATA%\chsum; everywhere else ~/.local/share/chsum, macOS included.
A ~/.chsum from an earlier version moves there on the next run, once, with a
line on stderr naming both paths.
Nothing is written to a transcript, with one exception: chsum name appends an
ai-title record, the shape Claude Code itself appends. That append is skipped
for a translated session, whose file is rebuilt from its original and whose
original is never read back by chsum — the name holds in names.json, which is
what every listing reads.
Which version am I running
chsum --version
chsum 2.0.0 (dc12598) · python 3.10.11 · darwin
The version comes from the checkout's own pyproject.toml, and the commit
beside it is what built it. An editable install freezes the packaged metadata at
install time and nothing refreshes it on its own, so after a version bump the
two can disagree — the line then says so and names the fix:
chsum 2.0.1 (1c8e002) · packaged metadata says 2.0.0 and no longer describes
what runs; `pipx install --editable . --force` from the checkout refreshes it
Reporting something that looks wrong
--debug goes on the end of any command. It prints what that run read, ran and
resolved, beneath the normal output:
chsum digest ch_8b0a671d… --stdout --debug
--- chsum debug ---
invocation: chsum digest ch_8b0a671d… --stdout --debug
cwd: ~/src/chsum
projects: ~/.claude/projects/ (…/ below)
chsum 1.2.0 (9162a1d dirty) · python 3.10.11 · darwin · exit 0
files (1)
ch_8b0a671d… meta,turns 947.1K 671 recs …/-Users-…-chsum/9a9e9ac5-….jsonl
procs (0)
steps (3)
resolve_ref via=argv ref=ch_8b0a671d…
_parent_path ref=ch_8b0a671d… uuid=9a9e9ac5-…
_load_store dir=-Users-…-chsum files=126 sessions=6
reproduce
chsum digest ch_8b0a671d… --stdout
--- end chsum debug ---
No transcript text is copied — only paths, refs, record ids and counts — so the block names the record behind a line that looks wrong rather than carrying it. It assumes the reader is on the same machine: paste it into a session in the chsum checkout and the files it names are still there to open.
steps is where a wrong line usually comes from. Each one is a resolution with
its inputs and its result, including the fallbacks that print nothing during a
normal run: _stamp_turns … unstamped=126 stamped=126 transcripts=38 (turn files
from before the store was stamped, resolved against this project's transcripts),
_turn_checkpoints … checkpoint=4 transcript=2 (two turns had no checkpoint
covering them), _run_chunk … failed=1 (one summariser call died and its gap
says so). Repeated steps collapse in the middle, and files and procs cap at
24 rows with the remainder counted.
Without the flag nothing is recorded and nothing is printed.
Notes on correctness
Several things here are non-obvious and were established by measuring, not assuming:
- Duration excludes idle time. Sessions get resumed hours or days later, so first-record-to-last-record wildly overstates effort — one session in the corpus reads as 92 hours. Gaps over 30 minutes are treated as "walked away".
- Most "user" records aren't from you. They're tool results, interrupts, and
harness scaffolding. Those are filtered out;
prompts:counts what you typed. A!run is something you did, not something you said, and is not a prompt. - An API error is not a reply. A record carrying
isApiErrorMessage— "Prompt is too long", a session limit — is how the session stopped, and is reported as the notice rather than quoted as the last thing Claude said. - Subagent transcripts aren't conversations in their own right and never appear
in the listing, matching
claude-history's discovery rules. claude-historyhas no per-agent ref.--subagentsinlines agent messages into the parent read untagged, so they can't be sliced apart. Sidecars are parsed directly. Every digest reads the JSONL this way, parent included.- An agent's report is a record, not a tool result. An async agent's own
tool_resultholds launch metadata; the report arrives later as a<task-notification>record, which is what--agentsreads. - An agent's last message isn't necessarily its conclusion, so the section is Last thing it said. An interrupted agent ends mid-thought.
- Agent counts take the larger of two sources —
Agent/Taskcalls in the parent, and sidecars on disk. Sidecars go missing; an agent that spawns its own outnumbers the visible calls. - Scratch paths (
/tmp, scratchpads, plan files) are excluded from "files changed" so the work log shows real project changes.
Prose, and where it's allowed
The Summariser seam at the bottom of chsum.py has its first backend:
HaikuSummariser, used only by recap, via claude -p --model haiku — no
SDK, no key handling, your existing Claude Code auth signs the call. A TL;DR is
the one thing extraction can't produce, and a running session is where you're
most likely to want one before the transcript catches up.
The rule for any backend: it gets the already-extracted material, and its output is additive — layered on top of the verbatim record so a wrong sentence can always be checked against the quotes beneath it.
If you do go local, note that the model in mlx-community/DeepSeek-R1-Distill-Qwen-14B-MLX
is 139 GB of unquantised weights. The 4-bit build is …-14B-4bit at 8.32 GB. On a
16 GB machine the binding constraint is KV cache, not context length: this architecture
costs 192 KB/token at fp16 (96 KB with kv_bits=8), so after 8.32 GB of weights you get
roughly 18k–36k tokens of usable input, not the 131k the config advertises.
Release files for chsum 3.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| chsum-3.0.0.tar.gz | 191.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| chsum-3.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 353.5 kB
Release files / chsum-3.0.0.tar.gz
| Download URL | chsum-3.0.0.tar.gz |
|---|---|
| Size | 191.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
379e2fdc9d0a4a0330bb49347e614804bd21da647d1a02649eec79fe0745dc2e
|
|
BLAKE2b-256 checksum How to use checksums |
a17f35bcc42f363d8b37f449f3d84b45ac6400596d9cef22921b559c42d399f0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.
Transparency logRelease files / chsum-3.0.0-py3-none-any.whl
| Download URL | chsum-3.0.0-py3-none-any.whl |
|---|---|
| Size | 162.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
152bb499df4271b0e5bea4bfd5da4521a70baae19fa4964579adfb964fe9f786
|
|
BLAKE2b-256 checksum How to use checksums |
18ba72711481a804a2b01858070ba301dd25df5c7a4a1e4eab23f839c21f2fa1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.
Transparency log