Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Persisting the Ledger

By default branch-tally report reads Claude Code’s transcripts live. That has limits:

  • Transcripts are purged. Claude Code deletes them after cleanupPeriodDays (30 days by default). A branch that took longer than that, or a report run after the cleanup, comes up short.
  • Transcripts are local. Work on a branch often happens on more than one machine. Each machine only sees its own sessions.
  • Worktrees come and go. Subagent work in a git worktree is easiest to attribute while the worktree still exists.

The ledger fixes that. branch-tally record writes a branch’s attributed usage to a file in the repository, .tally/ledger.jsonl. You commit that file like any other. branch-tally report --from-ledger then reports from it, with or without the transcripts.

flowchart LR
    subgraph A[Machine A]
        TA[transcripts] -- "branch-tally record" --> LA[".tally/ledger.jsonl"]
    end
    subgraph B[Machine B]
        TB[transcripts] -- "branch-tally record" --> LB[".tally/ledger.jsonl"]
    end
    LA -- "git commit, push" --> M["feature branch<br/>(merge=union keeps both sides' lines)"]
    LB -- "git commit, push" --> M
    M -- "branch-tally report --from-ledger" --> R[report]

Set up your repository

Add this line to your repository’s .gitattributes:

.tally/ledger.jsonl merge=union

merge=union tells git to keep both sides’ lines when two branches have each appended to the ledger, instead of reporting a conflict. branch-tally does not add this line for you. Without it, two branches that both recorded will conflict when merged.

If you use a different path with --ledger, put that path in .gitattributes instead.

If you commit the hook captures file too, give it the same treatment: .tally/captures.jsonl merge=union.

Record

branch-tally record              # the current branch
branch-tally record feat/thing   # a named branch

record selects the branch’s usage exactly as report would, then appends one line per API call to the ledger:

appended 3, skipped 0, branch feat/x
  • It only writes the file. It does not git add or commit anything. Commit the ledger yourself.
  • It is safe to run again. Each line is keyed by the API call’s request id and the branch it was recorded for. record appends only keys the ledger does not hold yet, so a second run reports appended 0, skipped 3.
  • It creates the file and the .tally/ directory when they are missing.
  • It stores the mode. record takes the same --mode, --strict, --timeline and --work-branch options as report, and the config file’s mode and work-branch. Each line stores the mode it was selected under.
  • In claim mode, one API call can count toward two branches. It is then recorded once for each branch you record.

Run record on every machine that did work on the branch, before its transcripts are purged, and while any worktrees the work used still exist. Then commit and push the ledger.

The full option list is in the CLI reference.

Record automatically when a session ends

Remembering to run record before cleanupPeriodDays runs out is easy to get wrong. The optional branch-tally-record Claude Code plugin does it for you: its SessionEnd hook runs branch-tally record every time a session in the repository ends.

/plugin marketplace add IAmSkweetis/branch-tally
/plugin install branch-tally-record@branch-tally

The hook records the branch the session’s directory has checked out when the session ends, with the mode and work-branch from your config file. It is the same command as above, so the same rules hold: it only writes the file, a second run appends nothing new, and you still commit and push the ledger yourself. It needs branch-tally on your PATH, and it never blocks the session, even when record fails.

Claude Code gives a SessionEnd hook 1.5 seconds by default, and a plugin can’t raise that. record reads every transcript for the repository, so a long history can need more. Set CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS (in milliseconds) or use a hook in your own settings with a timeout, as described in SessionEnd time limit. If record is stopped part way, it catches up later: it appends only what the ledger does not already hold, so the next session end, or a record by hand, writes the rest, provided it runs before the transcripts are purged.

A branch worked on from several sessions, or with sessions that ended on another branch, may still need a record by hand for the branches the hook did not see. Installing the plugin, and the companion branch-tally-capture plugin, is covered in Integration.

Report from the ledger

branch-tally report --from-ledger
branch-tally report --from-ledger --with-live

--from-ledger reports only from the ledger’s lines recorded for the requested branch. It works on a machine with no transcripts at all, such as a fresh clone. Every other report option still applies: --json, --pr, --redact and the rest.

--with-live also reads this machine’s live transcripts and combines them with the ledger. When a call appears in both, the ledger’s line is used, so nothing is counted twice. Use it when part of the work is recorded and part is still only in local transcripts. --with-live without --from-ledger is a command-line error (exit 2).

Prices are applied when you report, not when you record. The ledger stores token counts, so a report from the ledger uses the price table in effect at report time.

What a ledger report tells you on stderr, as counts only:

NoteMeaning
note: unattributed counts unavailable from the ledgerA live report can say how many calls in the repository could not be attributed to any branch. The ledger only holds the requested branch’s calls, so it can’t.
note: ledger mixes modes (claim, timeline); the numbers combine themThe branch was recorded under more than one attribution mode.
note: skipped N ledger line(s): …Lines that were malformed or written by an unsupported ledger version were skipped and counted.
note: N ledger request(s) had conflicting lines; kept the latestTwo lines with the same key held different data. The one with the latest recorded_at is used.

A duplicate line, such as one left by a union merge or by recording twice, is collapsed silently: it is not a conflict.

If the ledger has no lines for the branch, report --from-ledger fails with No ledger records found for branch '…' (exit 1). If the ledger file does not exist, it fails with no ledger at <path> (exit 1).

Where the file lives

The default is .tally/ledger.jsonl at the root of the git checkout that contains the current directory (or the --repo directory). Run from inside a git worktree, that is the worktree’s own root, so the ledger is committed on the worktree’s branch and merges back with it.

--ledger <path> uses another file, on both record and report --from-ledger. Outside a git checkout, --ledger is required: without it, record and report --from-ledger fail (exit 1) rather than write a stray ledger.

--ledger only has an effect on report together with --from-ledger.

What is stored

One JSON object per line, append-only. A line holds:

FieldHolds
vThe ledger schema version, currently 1.
request_idThe API call’s request id. With recorded_for, the line’s key.
harnessThe agent harness, claude-code.
session_idThe Claude Code session id.
agent_idThe subagent’s id, when a subagent made the call.
tsThe call’s timestamp.
modelThe model id.
recorded_forThe branch record was run for.
modeThe attribution mode: claim, strict or timeline.
branchThe branch attribution assigned to the call.
basisHow that branch was decided, such as stamped by the transcript, rolled up from a subagent’s dispatcher, or captured from hook captures.
kept_byWhy the call was kept for the branch: its own branch matched, or its session was claimed.
skill, agentThe skill and named agent that made the call, if any.
subagentThe subagent’s workspace and ids, for subagent calls.
originWhere the session ran: "main" for the main checkout, or {"worktree": "<name>"} with the worktree’s name relative to the repository.
usageToken counts: input, 5-minute and 1-hour cache writes, cache reads, output and thinking.
recorded_atWhen the line was written, in UTC.

What is never stored: message content (prompts, responses, tool input or output, code), the working directory, or any path. A worktree’s transcript directory name encodes its full path, including your user name, so the ledger keeps only the part after the main checkout’s name. A worktree outside the main checkout is stored without a name, and record says so: note: N record(s) from a worktree outside the main checkout were stored without a worktree name.

Redaction and the ledger

The ledger stores real identifiers: session ids, agent ids, skill and agent names and full timestamps. record has no redaction option, and the config file’s redact setting does not apply to it. Anyone who can read the repository can read those identifiers.

--redact applies to report output, including a report from the ledger: branch-tally report --from-ledger --redact hides the identifiers in what it prints, while the file itself is unchanged.