Skip to content

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.

<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 .git file 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_touch lands in the touched FILE’s own repository ledger — the same place stamp/verify later look — while session_start/session_end land 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 why file_touch carries its own toolVersion and model: the touched repo’s ledger may never see a session_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 under worktrees/), 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 a session_start line there itself, marked with synthesizedFrom (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 real session_start did 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.

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.

  • 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 model field when present; otherwise a bounded tail-scan of the payload’s own transcript_path for the newest assistant line’s message.model (the vendor’s authoritative record; synthetic placeholders are skipped). This reads one field from the transcript’s last window — never conversation content. file_touch lines 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) or AI_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.
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.

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.

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.
  • stamp selects file_touch lines matching one sessionId and one document, derives agent/agentVersion from that session’s session_start line, and uses the latest witnessed touch as the document’s modified value (which is what makes stamping idempotent).
  • verify re-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 wasGeneratedBy id under urn: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_end windows and attribute files via file_touch lines — the same selection discipline: only sessions the ledger witnessed touching a file may be named for it.

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.