The session ledger: format contract
The session ledger: format contract
Section titled “The session ledger: format contract”The session ledger is the file the mif-provenance capture hooks append and the
stamp/verify verbs and corpus report read. This page is its contract:
the fields below are load-bearing for every consumer, and at least one
consumer outside this plugin — a commit-trailer generator sharing the same
observation stream — is an anticipated reader. A change to this format must
consider both consumer families, not just the verbs in this repository.
Location and lifecycle
Section titled “Location and lifecycle”<git-dir>/ai-provenance/session.jsonl<git-dir>is the repository’s own git directory (.git/, or the worktree-private git dir a linked worktree’s.gitfile names), so the ledger is never committed and never leaves the machine. Two worktrees of one repository therefore keep separate ledgers.- Which repository: a
file_touchlands in the touched FILE’s own repository ledger — the same placestamp/verifylater look — whilesession_start/session_endland in the session cwd’s repository. A session parked in one repo writing into a sibling repo therefore witnesses the touch where the lookup happens (which is whyfile_touchcarries its owntoolVersionandmodel: the touched repo’s ledger may never see asession_start). - Non-git launch cwd (issue #148): when the session’s own launch cwd is
not itself inside a git repository at all (a bare multi-repo workspace
root, for example — clones under
repos/, worktrees underworktrees/), the real SessionStart hook has no git dir to resolve and writes nothing, anywhere. The PostToolUse hook detects that specific case at the first touch to each repo’s ledger and synthesizes/replays asession_startline there itself, marked withsynthesizedFrom(see below) so a reader can tell it apart from one the real SessionStart hook wrote. This is narrower than the “session parked in repo A, editing repo B” case just above: there, a realsession_startdid land somewhere, just not in every ledger the session touches, so nothing is synthesized. - Outside a git repository, capture disables; no alternative store is invented.
- Append-only semantics: writers add whole lines and never rewrite or
reorder existing ones. Truncation/rotation is the repository owner’s
responsibility (delete the file or the
ai-provenance/directory at will — consumers treat an absent ledger as “no witnesses”, never as an error). - Readers must skip unparseable lines (a torn concurrent append, a hand edit) rather than fail: the ledger is evidence, and losing one line must not invalidate the rest.
Line format
Section titled “Line format”One JSON object per line (JSON Lines). Every line carries:
| Field | Type | Meaning |
|---|---|---|
v |
integer | Format version, currently 1. |
event |
string | session_start, file_touch, or session_end. |
sessionId |
string or null | The session_id from the payload, else $CLAUDE_CODE_SESSION_ID. |
ts |
string | ISO-8601 UTC timestamp written by the hook at append time. |
Every recorded fact is witnessed, never guessed: a fact neither the
payload, the hook’s environment, the vendor transcript, nor the repository’s
own files carried is recorded as null.
Where the facts come from
Section titled “Where the facts come from”- Payload — every vendor-documented field for the event (
prompt_id,permission_mode,effort,agent_id,agent_type,source,session_title,reason, …). - Model — the SessionStart payload’s optional
modelfield when present; otherwise a bounded tail-scan of the payload’s owntranscript_pathfor the newest assistant line’smessage.model(the vendor’s authoritative record; synthetic placeholders are skipped). This reads one field from the transcript’s last window — never conversation content.file_touchlines re-witness the model per touch, because mid-session model switches are real. - Runtime environment — every variable whose name matches the Claude
Code / agent runtime (
CLAUDE*,ANTHROPIC*,AI_AGENT,CLAUDECODE, terminal session ids), captured structurally so a new CLI version’s new variables land without a code change — minus any credential-shaped name (KEY/TOKEN/SECRET/PASSWORD/CREDENTIAL/AUTH), which is never recorded regardless of prefix. Values are truncated to 512 chars. - Tool version — derived from the CLI’s versioned exec path
(
CLAUDE_CODE_EXECPATH) orAI_AGENT; no payload carries it. - Host — platform, arch, node version, OS release, hostname, username (the ledger never leaves the machine; naming the checkout is attribution signal, not exfiltration).
- Repository — branch and HEAD sha resolved from the git dir’s own files
(worktree-aware,
packed-refs-aware); no git binary is spawned.
session_start
Section titled “session_start”| Field | Type | Meaning |
|---|---|---|
tool |
string | Always claude-code (the witnessing surface is its hook set). |
toolVersion |
string or null | Derived from the CLI exec path / AI_AGENT. |
model |
string or null | Payload model, else the transcript tail-scan. |
source |
string or null | startup, resume, clear, … |
sessionTitle |
string or null | Payload session_title. |
permissionMode |
string or null | Payload permission_mode. |
effort |
string or null | Payload effort, else $CLAUDE_EFFORT. |
promptId |
string or null | Payload prompt_id. |
agentId, agentType |
string or null | Present when running as/inside a subagent. |
transcriptPath |
string or null | The session transcript the payload named. |
cwd |
string | The payload’s working directory. |
env |
object | The allow-listed runtime environment map (see above). |
sys |
object | platform, arch, nodeVersion, osRelease, hostname, username. |
git |
object | branch, headSha at session start. |
synthesizedFrom |
string, absent when real | Present only when this line was replayed by PostToolUse rather than the real SessionStart hook (issue #148, non-git launch cwd) — currently always post-tool-use:non-git-launch-cwd. |
file_touch
Section titled “file_touch”Appended by the PostToolUse hook for Write, Edit, and MultiEdit.
| Field | Type | Meaning |
|---|---|---|
tool |
string | Always claude-code. |
via |
string | Write/Edit/MultiEdit, or stamp (see below). |
filePath |
string | The touched file’s canonical absolute path (resolved against the payload cwd, symlink aliases flattened). |
model |
string or null | The transcript’s newest model at touch time. |
toolVersion |
string or null | The CLI version, per touch (see “Which repository” above). |
promptId |
string or null | Which prompt the touch happened under. |
permissionMode, effort |
string or null | As at session start, per touch. |
agentId, agentType |
string or null | The touching subagent, when one. |
git |
object | branch, headSha at touch time. |
contentHash |
string or null | sha256:<hex> of the file’s bytes at touch time. |
contentBytes |
integer or null | The file’s size at touch time. |
The contentHash is the strongest witness in the ledger: a consumer can
prove whether today’s file is still byte-identical to what the session
wrote. The ledger stores the hash, never the content.
A successful stamp that changes bytes appends its own file_touch with
tool: "mif-provenance", via: "stamp", and the post-stamp content hash —
so the newest recorded hash for a document always matches the bytes on disk
even on the auto-stamp path. Its ts reuses the witnessed touch time the
stamp derived modified from, keeping re-stamps byte-idempotent.
session_end
Section titled “session_end”| Field | Type | Meaning |
|---|---|---|
reason |
string or null | The SessionEnd reason, when the payload carried one. |
permissionMode |
string or null | Payload permission_mode. |
promptId |
string or null | Payload prompt_id. |
git |
object | branch, headSha at session end. |
How consumers use it
Section titled “How consumers use it”stampselectsfile_touchlines matching onesessionIdand one document, derivesagent/agentVersionfrom that session’ssession_startline, and uses the latest witnessed touch as the document’smodifiedvalue (which is what makes stamping idempotent).verifyre-derives the same expectation and diffs it against the document’s actual block, field by field.- The corpus report classifies a document as witnessed by the stamp marker
(a
wasGeneratedByid underurn:mif:activity:claude-code-session:), downgrading to asserted when a supplied ledger contradicts it. - A commit-trailer consumer can bound a commit’s sessions via
session_start/session_endwindows and attribute files viafile_touchlines — the same selection discipline: only sessions the ledger witnessed touching a file may be named for it.
What the ledger is not
Section titled “What the ledger is not”It is a local, unsigned witness: anyone with filesystem access can edit
it, which is why everything stamped from it carries trustLevel: user_stated as a hard ceiling and why confidence is never derived from it.
It records tool events, not intent, and it never contains document content —
only paths, identifiers, and timestamps.