AI sessions
Where AI attribution comes from, the note format madelog reads, how a note becomes an ai_session event, which tools map to which names, how the Claude Code prompt is found, and how to keep a session off the page.
madelog does not watch your editor and does not read your AI tool's logs. It reads the attribution that Git AI writes into git notes at commit time and turns it into ai_session events. Git AI is the only source, and the note format is the only thing madelog knows about it.
Install Git AI
Git AI is a separate, open-source tool that hooks into AI coding agents and records which lines they wrote. Install it once per machine:
$ curl -sSL https://usegitai.com/install.sh | bash> powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://usegitai.com/install.ps1 | iex"The binary lands in ~/.git-ai/bin and is linked into ~/.local/bin. There is no per-repository setup: Git AI attaches to the agents themselves, not to git hooks. madelog onboard reports whether git-ai is on your PATH; the hook also looks in ~/.git-ai/bin directly, because a hook started by a GUI client may not see your PATH.
Opt in
Recording is a choice made at madelog init, as the fourth question:
Record AI sessions from Git AI? [Y/n]The answer is stored in the intent event as record_ai_sessions, and the hook reads the newest intent event before looking for a note. To change it later, run madelog init again with --record-ai-sessions or --no-record-ai-sessions and the other three answers; that appends a new intent event and leaves the rest of the log alone. With recording off, commits are still recorded and the page's attribution section says no attribution was recorded.
Where the notes live
Git AI writes one note per commit to the ref refs/notes/ai. madelog reads that ref through libgit2 and nothing else; refs/notes/commits, refs/notes/ai-remote/* and refs/notes/ai-display are ignored. Notes are written locally at commit time, so on your own machine they are present. They are not fetched by a normal git clone; to get them on another machine run:
$ git fetch origin '+refs/notes/ai:refs/notes/ai'git notes --ref ai show <sha> (or git ai show <sha>) prints the note for a commit.
The note format
A note is a text blob: attestation lines, one line that is exactly ---, then one JSON object. madelog splits at the first --- line. This is a real note for one commit of src/reconcile.rs, 41 lines long, of which a Claude Code session wrote the first 27:
src/reconcile.rs
s_2c8a703bb1fd71::t_d92697dfcc1b35 1-27
h_ea2010baa7a42b 28-41
---
{
"schema_version": "authorship/3.0.0",
"git_ai_version": "1.7.6",
"base_commit_sha": "ec66d42714214edf41acca2c555bfd13b6682c44",
"prompts": {},
"humans": {
"h_ea2010baa7a42b": { "author": "Ana Kovač <ana@example.com>" }
},
"sessions": {
"s_2c8a703bb1fd71": {
"agent_id": { "tool": "claude", "id": "aa2b7669-72a3-4ce8-8fbf-62fd9caee4f5", "model": "claude-fable-5" },
"human_author": null
}
}
}Attestation rules, as madelog applies them:
- A file path sits at column 0, quoted with
"if it contains whitespace. Each entry under it is indented by two spaces: a key, a space, and 1-indexed line ranges such as1-4,9,12. - A key starting with
s_names a session in thesessionsmap; the part after::is a trace within the session and is dropped. A key starting withh_names a human in thehumansmap. A key with neither prefix (7 or 16 hex characters) names an entry in the legacypromptsmap. - Later entries override earlier ones for overlapping lines. A key that is in no map is counted as unknown and produces nothing. Lines with no entry have unknown provenance.
schema_versionis required and isauthorship/3.0.0at the time of writing.humansandsessionsmay be absent;promptsmay be{}.
From note to event
One ai_session event is written per session key that has at least one attributed line, anchored to the commit, in the order the keys first appear. The note above became this event:
{
"seq": 6,
"id": "01a0b381-e2df-7002-92b7-c2716ed68011",
"ts": "2026-09-18T07:53:49.023Z",
"kind": "ai_session",
"commit_sha": "77433d9561f61fea916756335e9efa4479ee550f",
"payload": {
"accepted": true,
"files": [
{
"ai_lines": 27,
"ai_lines_modified_by_human": 0,
"path": "src/reconcile.rs"
}
],
"model": "claude-fable-5",
"prompt": "Write src/reconcile.rs: load two CSVs (amount, reference), index invoices by amount in cents, and count bank rows that match an invoice by amount and reference.",
"response_excerpt": null,
"session_id": "aa2b7669-72a3-4ce8-8fbf-62fd9caee4f5",
"tool": "claude-code"
},
"prev_hash": "7a40a99db12c54e74ed7d909be8c7b127ff0055e213678a47fb323111d396bca",
"hash": "ac596f46c8c4b09e2d563cb11e532f3651f0d0b1beac85fcf8b5c55a455925ca"
}| Field | Value |
|---|---|
tool | agent_id.tool, mapped as below; other when the note names none |
model | agent_id.model, or null |
session_id | agent_id.id; when there is none, the s_ key itself |
files | One entry per file with at least one line attributed to this session; ai_lines is the count of those lines at commit time |
ai_lines_modified_by_human | 0 for s_ sessions. For a legacy prompts entry, its overriden_lines spread over the files in proportion to their line counts, largest remainder first, never more than a file's own count |
accepted | Always true: a key with attributed lines wrote lines that survived the commit |
prompt | The first prompt of the session, for Claude Code only, best effort; otherwise null |
response_excerpt | Always null |
Human keys (h_) produce no event. A note with only human lines produces nothing, and the commit is recorded as usual.
Which tools
Git AI names the agent in each note. madelog keeps the name with two exceptions:
| Git AI writes | madelog stores |
|---|---|
claude | claude-code |
github-copilot, github-copilot-cli, anything starting with github-copilot | copilot |
cursor, codex, windsurf, gemini, amp, droid, pi, opencode, cline, continue-cli, firebender, claude-web, cursor-agent, codex-cloud, devin, and any name added upstream | unchanged |
The name is what the page prints ("claude-code session", "cursor session"). A tool added upstream shows up under Git AI's name for it rather than disappearing.
Prompt lookup for Claude Code
For tool == "claude-code" the session id is the Claude Code session UUID, and Claude Code keeps that session's transcript at ~/.claude/projects/<encoded working directory>/<session id>.jsonl. madelog encodes the repository's working directory the way Claude Code does: the trailing slash is dropped, then every character that is not an ASCII letter or digit becomes -.
/Users/ana/projects/ledger → ~/.claude/projects/-Users-ana-projects-ledger/aa2b7669-72a3-4ce8-8fbf-62fd9caee4f5.jsonlFour directory names are tried in order: the working directory as git reports it, the same after resolving symlinks, and each of those under the older rule where only / and \ become -. The first file that exists wins. A session id that is empty, starts with ., or contains anything other than letters, digits, -, _ and . is refused, so a note can never point the lookup outside that directory.
The transcript is JSON Lines. madelog reads it line by line and takes the first line whose type is user, whose isMeta is not true (Claude Code's own injected entries, such as command output and reminders, carry isMeta: true), and whose message.content is a non-empty string or an array with at least one text block. Whitespace is collapsed to single spaces, the text is cut to 500 characters, and it is stored as prompt.
Everything here is best effort. A missing file, an unreadable line, a renamed directory, or a prompt that matches a privacy.redact pattern all mean prompt: null, never an error. The page then says "No prompt was recorded for this session (not captured, or redacted)." MADELOG_CLAUDE_PROJECTS_DIR points the lookup at a different directory, which is how the tests avoid real transcripts.
Redaction
privacy.redact in .madelog/config.toml is a list of regular expressions. The same list serves two purposes: a commit file whose path matches gets no diff excerpt, and a prompt whose text matches is dropped whole. A prompt is not masked, because a masked prompt still shows its shape. The patterns are read when the hook runs; a session recorded before you added a pattern keeps its prompt in the chain, and hiding it is the way to keep it off the page.
What is not captured
Prompt and response text, in general. Git AI notes carry no prompts and no model output, by design. Transcripts stay in each tool's own storage. madelog reads exactly one thing outside git, the first user message of a Claude Code transcript, and nothing else. response_excerpt exists in the schema and is always null.
Anything outside the commit. A session that produced no line that survived to a commit leaves no note and no event. Sessions in other repositories, sessions whose lines you deleted before committing, and chat that changed no file are invisible.
Anything outside git. No keystrokes, no editor state, no untracked files.
What "reworked by hand" means
The page's attribution section has three figures: AI-proposed lines accepted unchanged, AI-proposed lines reworked by hand, and lines written by hand. The middle figure comes from ai_lines_modified_by_human.
A Git AI note records final authorship at that commit. A line attributed to a session was AI-written and committed unchanged. A line you rewrote before committing carries no attribution at all; it is simply not an s_ line. So from one current-format note the reworked count is always 0, and the page's middle figure is 0 for any project recorded with Git AI 1.7 or later. Only the legacy prompts record format reports overriden_lines, and only notes in that format produce a non-zero figure.
What the record does show is rework across commits. A commit that changes files which carried AI lines in an earlier commit, within 24 hours and with a fix-like subject, is a fix_after_ai pattern and becomes a moment: "d366a14 reworked AI-written code in src/import.rs two hours after fb7eeec". That is where AI code being revised shows up today.
Hiding a session
A session you do not want on the page stays in the chain and is withheld from the page. The page shows a dashed point on the timeline, "AI session hidden by the author" in the record list, and "1 session hidden by the author before publishing" in the badge. Hiding is itself a recorded event, so the chain stays whole.
Find the event id and hide it:
$ madelog log --json | grep -B4 '"kind": "ai_session"' | grep '"id"'
"id": "01a0b381-e2df-7002-92b7-c2716ed68011",
$ madelog hide 01a0b381-e2df-7002-92b7-c2716ed68011
hidden. The session stays in the chain; the page will say one session is hidden.Or press h on the session in the preview that madelog publish opens; the same line is printed after the preview closes. Only ai_session events can be hidden:
$ madelog hide 01a0b381-e273-7473-93a7-566c69a0eb6d
madelog: only ai_session events can be hidden; event `01a0b381-e273-7473-93a7-566c69a0eb6d` is a commit
$ madelog hide 0192c6f2-4a1e-7b3c-9d0e-000000000000
madelog: no event with id `0192c6f2-4a1e-7b3c-9d0e-000000000000` in this logHiding the same session twice records nothing the second time: already hidden. Nothing was recorded; the page already leaves this session out., exit 0. The page counts one hidden session either way. There is no unhide in this version: a hidden session stays hidden on every later publish.
Warnings you may see
When a commit has no note and git-ai is not installed, the hook prints once per process, on standard error, and the commit goes through:
madelog: Git AI is not installed, so no AI sessions are recorded.
madelog: install it with: curl -sSL https://usegitai.com/install.sh | bashA commit with no note while Git AI is installed prints nothing: a commit with no AI in it has no note either. A note madelog cannot parse is skipped with one line naming the reason, and the commit is still recorded:
madelog: a Git AI note could not be read, so its AI sessions are skipped (the Git AI note has no `---` line between attestations and metadata)A note that is not valid UTF-8 prints the same line with the note is not valid UTF-8. Neither warning affects the commit: the hook always exits 0.