An AI coding session is trapped: one terminal, one machine, a proprietary log format, one pair of eyes. When it ends you're left with changed files and a scrollback buffer.
agit turns the session itself into an open artifact. It imports a runtime's native log into an append-only, hash-chained JSONL event log — and every feature is a view over that log. agit does not build an agent; it sits above every agent, the way git sits above every editor.
(a synthetic fixture session — real ones look the same, only longer)
agit is not an observability platform. Those ask you to instrument your agents with an SDK, and they show you what that instrumentation captured. agit reads the logs your runtime already wrote, on your own machine, with nothing to adopt in advance. And because every event carries content hashes, agit can prove when a log is incomplete rather than quietly presenting a partial picture as the whole story.
npm install -g agitsh
Or, for contributors, from source:
git clone https://github.com/agitHQ/agit && cd agit
npm ci && npm run build && npm link # `agit` is now on your PATH
-
agit import <session | bundle>— ingest a native session into.agit/sessions/<id>/events.jsonl, or adopt an agit log someone sent you (aprbundle, a downloaded share log) — auto-detected, verified before it is stored, and kept byte for byte so the sender's hashes still check out. Adapters for native logs, also auto-detected: Claude Code (~/.claude/projects/<project>/<uuid>.jsonl), Codex CLI (~/.codex/sessions/<y>/<m>/<d>/rollout-*.jsonl), OpenClaw (its JSONL transcripts, or the agent database that holds them:agit import openclaw-agent.sqlite), Cline — both the SDK messages file new sessions land in and the 3.x task directories an installed history still sits in (agit import <globalStorage>/tasks/<id>) — Roo Code's task directories, which are the same layout in its own dialect, pi sessions (~/.pi/agent/sessions/--<cwd>--/*.jsonl), Hermes Agent's session database (agit import ~/.hermes/state.db), ATIF trajectories, LangGraph checkpoint databases (agit import checkpoints.sqlite), Gemini CLI recordings (~/.gemini/tmp/<project>/chats/session-*.jsonl), Kimi Code wire logs (~/.kimi/sessions/<work dir>/<session>/wire.jsonl), and OpenCode's session database (agit import ~/.local/share/opencode/opencode.db) — the same event log, the same verbs, whichever agent produced the session. A database holds sessions rather than a session: every one in it is imported, one line each, and--thread <id>names just one.agit import --allfinds every session those runtimes have written on this machine (~/.claude/projects,~/.codex/sessions,~/.openclaw/agents/*/sessionsand the agent database beside them,~/.gemini/tmp/*/chats,~/.kimi/sessions/*/*/wire.jsonl,~/.pi/agent/sessions/*/*.jsonl,~/.hermes/state.db,~/.local/share/opencode/opencode*.db,~/.cline/data/sessionsand~/.cline/data/tasks, and VS Code'sUser/globalStorage/{saoudrizwan.claude-dev,rooveterinaryinc.roo-cline}/tasks) and imports what is new;--latesttakes just the most recent one;--since 7dbounds the scan. A directory listing plus the ordinary import — no daemon, no hooks — and last month's sessions are found the same way as today's. Deterministic: the same input always produces byte-identical output. Credential-looking strings are redacted on the way in (see SPEC.md section 8 for exactly what is and isn't caught).--no-redactstores a session verbatim when redaction would mangle it;share,prandexport-htmlthen refuse that session until you pass--allow-unredacted, and re-importing without the flag puts redaction back. -
agit ls— list imported sessions: start, duration, events, files touched, and tags. Ids show as the shortest prefix still unique in the store. Narrow with--tag,--runtimeor--project, order with--sort started|events|files|id. -
agit tag <id> <tag>/agit note <id> "<text>"— what you thought about a session afterwards. Both live in a sidecar (notes.json) beside the log and never enter the chain: the chain is what the runtime did, and a tag changes.agit verifyneither sees nor is affected by them. -
agit rm <id>andagit gc --older-than 90d [--keep-tagged]— retention. Both confirm before deleting, and refuse outright when there is no terminal to ask unless--yesis passed.rmwarns first that any fork of the session loses its merge base, sinceagit mergereconstructs that from the log — it cannot know whether such a fork exists, since forks live wherever--output them with no registry to consult, and says so rather than guessing. -
agit show <id>— one-session summary: model, tools, token totals, per-file diffstat.--by-modelsplits it: what each model cost and how many files its edits touched. Tokens are exact; file attribution credits an edit to the model named by the nearest preceding event, and says so. -
agit stats— store-wide usage across every session and runtime: tokens, cache reads and writes, API calls, sessions and files touched, grouped--by day|model|runtime|projectand narrowed with--since. No prices are built in — they change, they differ per account, and a stale number that looks authoritative is worse than none. Pass your own rate table to get money:{ "currency": "USD", "per": 1000000, "models": { "claude-opus-5": { "input": 15, "output": 75, "cacheRead": 1.5, "cacheWrite": 18.75 } } }agit stats --by model --price rates.json. A model missing from the table is reported as unpriced, never costed at zero, and a group whose runtime records no cost events at all says so rather than showing a row of zeros. A fold over thecostevents each session already carries, so it needs no new data, and it says how many sessions it could not read rather than quietly leaving them out.--jsonfor scripts. -
agit grep <pattern>— search every imported session at once: "which session touched auth.py" (--path), "where did I run pytest" (--type tool.call). Matches the same one-line renderingreplay --timelineprints, so what you search is what you saw, and outputs one flat row per hit for piping onward. -
agit verify <id>— validate the hash chain; reports the first broken link, and detects truncation viameta.json. -
agit blame <file>andagit why <file>:<line>— from a line of code back to the session and event that wrote it, and from there back to the prompt that asked for it. Verifiable rather than merely recorded: the line traces to a hash-chained event, and every replayed step had to reproduce its own content hash. It inherits the §5.7 limit and says so — lines a shell command wrote read(no structured edit), and where a file changed outside structured edits, blame stops at that point and names it instead of guessing past it.agit linkprints theAgit-Session: <id>@<seq> <hash>trailer to anchor a commit to a session. -
Redaction controls — the built-in patterns (SPEC §8) can be extended and narrowed per project via
.agit/redact.json(or--redact-patterns):{ "patterns": [{ "label": "acme-token", "regex": "acme_[A-Za-z0-9]{20,}" }], "allow": ["sk-ant-EXAMPLE00000000000000", { "regex": "^sk-ant-example-" }] }Custom patterns catch internal token formats the built-ins cannot know about; the allowlist keeps documented example keys and test fixtures from being rewritten on import.
agit redact --check <log>is a dry run: it prints what would be redacted, by event type and payload path, with every sample masked, then re-scans the redacted result so the count can never under-report.--no-redactstores a log exactly as the runtime wrote it — for local-only stores;shareandprrefuse such a session unless--allow-unredactedis passed. Redaction still happens once, before hashing, at import: the chain never holds both versions of a string. A livesharere-converts from the native log rather than reading the store, and applies the same config, so what a viewer sees is redacted by the same rules the store was. -
agit replay <id>— step through events (n/p/g N), inspect any event, and show cumulative file state at any point (s, or--at N --statenon-interactively).--at Njumps straight to event N;--timelineprints the whole session one line per event. -
agit fork <id> --at N— branch a session at event N. The file tree is reconstructed from the log and verified: every replayed diff must reproduce its event's content hash, broken chains recover from runtime-recorded pre-edit content where it exists, and whatever cannot be verified is listed instead of written. Context is honestly lossy: the fork getsSEED.md, a deterministic mechanical summary (provenance, the task, last exchanges, file state) — not a transplant of the agent's mind.fork.jsonrecords the source session and fork-point hash, so provenance is checkable withagit verify. -
agit diff <a> <b>— what two sessions did differently: files each side touched, which ones converged on identical content, which diverged (with both hashes), and the work each spent getting there.agit diff <fork-dir>compares a fork against the parent it came from, starting at the fork point recorded infork.json. Comparison is by reconstructed content, so it inherits replay's blind spot and says so. -
**
agit merge <fork-dir>— bring a fork's files back: ordinary git three-way merge per file with the fork point as base (git merge-filedoes the merging). Trivial cases fast-forward, real conflicts get standard markers and a nonzero exit, and the merge — outcomes plus your--summaryof what the fork learned — is recorded in the fork'smerge.json. Not a merge of two minds; file-level, as promised. -
agit pr <id>— hand a session to a colleague as a directory: the full event log,meta.json, the reconstructed hash-verified tree,SEED.mdcontext, and provenance. They runagit importon the directory and get a session they can replay, fork and verify like any other — working context, not a read-only transcript. -
agit share <id | native.jsonl>— share a session through a relay, live while the agent is still running: the CLI tails the native log and streams events; teammates watch in a browser (timeline, diffs, token meter) and can send messages that land in your terminal. Watching is read-only by default: viewer messages reach the human at the keyboard, never the agent;--steeris the opt-in that changes that, below. A completed live stream is byte-identical to a full import — viewers can downloadevents.jsonlandagit verifywhat they watched. If the sharing CLI dies,agit share --resume <share-id>reattaches to the same link and pushes only the missing tail. Links expire (24h default) and sharing is opt-in per session, always. A stored session whose chain does not verify is refused —share,export,forkandprall name the failing event rather than publishing it. A session that lives in a database is shared the same way —agit share ~/.hermes/state.db --thread <id>(the id is optional when the file holds one session) — read as SQLite reads it, WAL sidecar folded in, so a running Hermes, OpenCode or LangGraph session streams as its rows land; totals a runtime records only for the whole session are held back until it ends, and a message OpenCode is still streaming (notime.completedyet — its parts grow in place) is held back until it completes. A runtime that rewrites earlier rows in place (a compression, a rewind) stops the share as the rewrite of streamed history it is. -
agit share <native.jsonl> --steer— let teammates redirect the agent, not just watch it. The share prints a steer key alongside the link; a viewer who enters it (on the page, or from a terminal withagit steer <link> "<text>" --steer-key K) sends a message to the agent instead of only to your terminal. Nothing is injected mid-run and nothing is typed on your behalf: the message is queued under.agit/steer/, and Claude Code's own hooks hand it over at the next turn boundary —Stop, so the agent picks it up instead of stopping, andUserPromptSubmit, so it rides along with your next prompt if the agent was idle. Both are the documentedhookSpecificOutput.additionalContextchannel, subject to Claude Code's own loop guard (eight continuations in a row, then it stops regardless). One-time setup per project:agit hook --configprints the two-linesettings.jsonfragment that runsagit hookat those events; it prints nothing unless a live share has queued something. Every steered message is shown in the sharing terminal first, attributed, and a message with a wrong key is shown too — labelled, and delivered to no one. The key never reaches other viewers; the relay forwards it to the sharer alone, who is the only party that can check it. Three runtimes are wired, because three document a turn-boundary channel: Claude Code (above, verified against the runtime); Gemini CLI, whoseAfterAgenthook takes a blocking decision whosereasonis sent to the agent as the next prompt with the history kept, and whoseBeforeAgenthook takesadditionalContextfor the idle case —agit hook --config gemini-cliprints that fragment; and pi, whose extension API lets a handler inject a message frombefore_agent_startand queue one fromagent_endwithpi.sendMessage(…, { deliverAs: "followUp", triggerTurn: true })—agit hook --config piprints a small extension that does exactly that by runningagit hook, to save as~/.pi/agent/extensions/agit-steer.ts. Both Gemini CLI's and pi's paths are derived from the source and not yet exercised against a running copy.--steeron a Codex or OpenClaw session is refused with that reason rather than promising a channel that does not exist. -
agit relay— the self-hosted relay behindshare: in-memory only, loopback by default, nothing persisted. PROTOCOL.md documents the (v0, unstable) wire protocol. -
agit push <id>/agit pull <link>— a remote is just a relay someone else runs.pushpublishes a verified session and exits, printing a link;pulladopts it somewhere else over HTTP. Pulling is the same code path as adopting aprbundle, so the relay is never trusted: the chain is verified before anything is stored, the events land byte for byte with the hashes the origin published, and a relay that alters a single byte serves a log that fails verification. Pushing twice reuses the same link unless you pass--force, and pushing a session that does not verify is refused, like every other publishing verb.agit relay --store <dir>gives the relay a disk, so a restart keeps every link working instead of dropping it — two flat files per share, no database and no dependency. Without it the relay is memory-only, which is still the default. That directory holds writer tokens, so treat it as a credential store; it is created0700with0600files, and on Windows those modes are advisory.agit share --static --detachprints the link and exits, for CI and scripts that want a URL rather than a process. -
agit export <id> --otel | --atif— feed the tools you already run, with a log that verifies.--otelemits OTLP/JSON spans following the OpenTelemetry GenAI semantic conventions: aninvoke_agentroot,chatchildren per model call with token counts,execute_toolchildren per tool.--atifemits a Harbor Agent Trajectory Interchange Format document (ATIF-v1.8), which Harbor uses for evals and fine-tuning and which its OpenHands adapter converts OpenHands event logs into. Both are folds over events already stored; neither changes SPEC.Span ids are the leading bytes of the event hash they came from, so a trace in Grafana points back at a specific line of a log you can
agit verify; the full hash rides along as an attribute, because 8 bytes is a convenience and not a proof. Output is deterministic — no random ids, no wall clock — and an unverified session is refused, since feeding an eval from a log agit cannot vouch for is how a verified pipeline quietly stops being one. The GenAI conventions are at Development stability and have never cut a release, so the emitted schema URL ends in-devand this will need updating;file.diffhas no home in either schema, so edits ride in each format's own extension field with the SPEC §5.7 lower bound stated beside them. -
agit export <id> --markdown— the session as a Markdown audit report for a PR body or a review ticket: provenance (head hash, chain verified, every signature and its verdict), usage totals asstatscounts them, the files touched with the SPEC §5.7 floor stated, and a timeline. Everything a log contributes is text, never structure: message bodies are fenced with a fence longer than any backtick run inside, names and paths sit in inline code built the same way, and table cells escape their pipes — a user message reading# AUDIT PASSEDstays inside its fence. Same gates asexport-html: an unverified session is refused, and an unredacted one needs--allow-unredacted. -
agit sign <id> --key <file>— bind a head to a key. The chain proves a log was not modified after it was chained; it says nothing about who chained it, because anyone can rebuild a perfectly valid chain over edited content and a matching head. A signature is the missing half, andverifycatches exactly that forgery:NOT OK: 31 events, chain intact, matches meta.json head, but a signature does not match: SIGNATURE DOES NOT MATCH (SHA256:+++sbwFo…): does not match this head — the log changed after signing, or the signature was never validEd25519, reading the
~/.ssh/id_ed25519you already have or any PKCS#8 PEM. Signatures live inmeta.json, never in the chain, so a session can be signed after import and by more than one person without rewriting an event — and they travel inprbundles, so the recipient can check them. Encrypted keys are refused rather than prompted for; agit never handles a passphrase. What it does not prove: when. The timestamp is signed, so it cannot be edited afterwards, but it is still a time the signer chose. Only a third-party RFC 3161 time-stamp makes that evidence, and agit does not issue one. SPEC §12 documents the signed payload so other implementations can verify without agit. -
agit mcp— serve the store to an agent over MCP, so the agent can ask its own verified history "have I solved this before?" instead of that being something only a human can do withgrep. Six read-only tools:agit_list,agit_grep,agit_show,agit_replay,agit_diffandagit_verify. Claude Code, Codex, Cursor, Gemini and Cline all speak MCP, so one server covers every runtime agit imports from. Point a client at it:{ "mcpServers": { "agit": { "command": "agit", "args": ["mcp", "--dir", "/path/to/project"] } } }Read-only by design. There is no tool that writes, and nothing arriving over this transport reaches
import,tag,rmor the redaction config. Every answer carries whether that session's chain verifies, andagit_verifyasks directly, so an agent knows what it is trusting. Session logs are untrusted input, so each payload is labelled as recorded data rather than direction — a label, not a sandbox, and worth the same scepticism as redaction.
--json on ls, show, show --by-model, verify, grep, diff
and export emits the structures agit already builds, so a script reads
the same numbers the table renders — full ids, ISO timestamps, real
integers. grep --json is one object per line (NDJSON); everything else
is one document. Errors stay on stderr, so a pipe into jq is always clean.
Session ids accept unique prefixes, git-style. The inspection verbs are
fully local: no server, no network calls, no telemetry. Only share talks
to a relay — one you run.
What each runtime's log lets agit do, at a glance; the reasons and edges are
in "What does not work yet" below. Verified means a file.diff whose
hashes are over bytes agit holds and the runtime wrote; window means an
update verifies only while agit already holds the file (a create, an
earlier verified edit, a complete read, or --base).
| Runtime | Reads | file.diff |
Live share |
--steer |
Derived from |
|---|---|---|---|---|---|
| Claude Code | ~/.claude/projects/<p>/<uuid>.jsonl, and <uuid>/subagents/*.jsonl |
Edit / Write, verified |
yes | yes (Stop / UserPromptSubmit hooks; verified against the runtime) | real logs |
| Codex CLI | ~/.codex/sessions/…/rollout-*.jsonl |
creates verified; updates within the window | yes | no | its structured edit records (real-rollout check open, #34) |
| OpenClaw | ~/.openclaw/agents/*/sessions/*.jsonl, openclaw-agent.sqlite |
apply_patch replayed with OpenClaw's rules; updates within the window |
yes, both | no | types + a real 2026.9.3 transcript |
| ATIF | trajectory.json |
none (no edit construct) | yes | no | Harbor's RFC |
| Cline SDK | ~/.cline/data/sessions/<id>/<id>.messages.json |
none (line endings depend on an OS the log omits) | yes | no | the messages contract v1 |
| Cline 3.x / Roo Code | <globalStorage>/tasks/<id>/ |
none (<final_file_content> is normalized text) |
yes | no | source at v3.89.2 / v3.20 + v3.54 |
| LangGraph | checkpoints.sqlite |
none | yes | no | its checkpoint schema |
| Gemini CLI | ~/.gemini/tmp/<p>/chats/session-*.jsonl |
none | yes (last message held until settled) | yes (AfterAgent / BeforeAgent hooks; source-derived) | source |
| Kimi Code | ~/.kimi/sessions/*/*/wire.jsonl |
none | yes | no | docs + source |
| OpenCode | ~/.local/share/opencode/opencode.db |
none | yes (a streaming message held until complete) | no | its schema |
| pi | ~/.pi/agent/sessions/--<cwd>--/*.jsonl |
write verified; edit within the window; complete reads seed fork |
yes | yes (an extension; source-derived) | docs + source |
| Hermes Agent | ~/.hermes/state.db |
write_file when Hermes's own byte count says untransformed; patch within the window |
yes (totals held until the end) | no | source |
Every database (LangGraph, OpenClaw, OpenCode, Hermes) is read with its WAL sidecar folded in, so a running writer's newest rows are seen. "Derived from source" means the adapter was built from the runtime's own recorder and checked against a fixture written the same way, not against a real log from that runtime; a real one that disagrees names its unmapped records in the import report.
SPEC.md is the most important artifact in this repo. Nine event
types (session.start, session.end, message.user, message.assistant,
tool.call, tool.result, file.diff, file.delete, cost), each
carrying a canonical
SHA-256 hash and the hash of the previous event. Tamper-evidence, stable fork
points, and independent verification of what an agent claims it did — all
fall out of that chain.
Said plainly:
- Twelve adapters, with different limits. Claude Code is the reference;
Codex is mapped from its own structured edit records. OpenClaw is mapped
from the
apply_patchtext it records, replayed with OpenClaw's own matching rules. pi'swritewrites its argument verbatim and itseditrecords the patch it applied, so both verify — a write against the bytes it wrote, an edit against content agit already holds. Hermes'swrite_fileverifies when its own post-write byte count says nothing was transformed, and itspatchagainst content agit holds. ATIF is a standard rather than a runtime, Cline's SDK format is a published contract and its 3.x task directory is read from the source of the last release that wrote it, LangGraph's and OpenCode's databases are read from each runtime's own schema, Gemini CLI's recording is folded the way its own loader folds it, and Kimi Code's wire log is folded the way its own replay folds it; none of the seven carries file content agit can hash — see below. - Codex updates have a verification window. Codex records a file's full
content when it creates one, but only a diff when it updates one — so
agit can verify an update only while it already holds that file's content
from earlier in the same session. An edit to a file that predates the
session is skipped and counted, never hashed on a guess — unless you supply
the base yourself:
agit import <log> --base <git-ref | directory>seeds the pre-session content from the commit the session started from, or a copy of the tree. It is checked, not trusted: the runtime's own diff still has to apply to it and the result still has to hash, so a wrong base skips exactly as no base does. Only files the runtime actually edited are consulted; nothing else enters the log, andmeta.jsonrecords which base was used. - OpenClaw has the same window:
apply_patchrecords the patch, not the file, so an update is verifiable only for a file the session created. - Codex renames are recorded as a delete plus a create — schema v2 gives
a rename an honest encoding, so the log says what the filesystem saw: the
old path gone, the new one created with the updated content. A rename
whose base predates the session is still skipped and counted, because
neither path has content agit could hash. Deletions are recorded
(
file.delete) whenever Codex logged the file's content. - Codex reasoning arrives encrypted and is dropped, counted.
file.diffcoverage is partial. Diffs come from structured edit tools (Edit/Write). Files changed through shell commands leave no diff event; file state from replay is a lower bound on what changed. Fork trees inherit this blind spot: a file the log never structurally edited is absent from the tree entirely, and a file deleted by a shell command still appears at its last logged content — only a structured deletion (file.delete) removes it.- No message injection mid-run, and steering is wired per runtime.
Sharing is watch-only unless the sharer passes
--steer, and even then a message is never fed into a running turn: it waits for the runtime's own turn boundary — Claude Code'sStop/UserPromptSubmithooks, Gemini CLI'sAfterAgent/BeforeAgent, pi'sagent_end/before_agent_startextension events — and is delivered through the channel each documents, which the agent weighs like any other context — a teammate's request, not a command from the keyboard. Codex, OpenClaw, Cline, Roo Code, Kimi Code, OpenCode, LangGraph, Hermes and ATIF sessions have no documented equivalent, so--steerrefuses them; a runtime that gains one gets wired the same way, per adapter, opt-in. - The relay speaks TLS only when you give it a certificate.
agit relay --cert <pem> --key <pem>serves HTTPS; otherwise it is plain HTTP on loopback, and binding beyond loopback without TLS is refused unless--insecureis passed. A tunnel or TLS-terminating proxy remains a perfectly good alternative (PROTOCOL.md). - Merge is file-level. Three-way content content merge only, and a
rename is two files. It uses
git merge-filewhen git is on PATH and a built-in three-way merge otherwise (--no-gitforces the built-in one); git is preferred because its results are what everyone's expectations are calibrated against, and the two are pinned to each other by differential tests. A file simply absent from a fork tree counts as untouched rather than deleted, because the tree records only what the log could reconstruct — butagit merge --session <id>reads the fork's own imported session and honours thefile.deleteevents it recorded after the fork point, deleting only where the removed content matches both the fork point and what the target holds today. Fork andprcontext seeding is a summary by design; you cannot inject history into a running agent. - An ATIF import has no file history.
agit import <trajectory.json>reads Harbor's Agent Trajectory Interchange Format, so anything emitting ATIF can be verified, replayed, searched and shared. But ATIF has no file-edit construct — a write is an ordinary tool call whose result is prose — so nofile.diffcan be emitted over content the document does not hold.blame,why,fork,mergeanddifftherefore have nothing to work with on such a session; everything else does. What the import cannot carry is counted and named rather than passed over, including edits that agit itself exported as hashes. - A Cline SDK import has no file history either, for a sharper reason.
agit import <id>.messages.jsonreads the format new Cline sessions land in from 4.0 (~/.cline/data/sessions/), which Cline documents as "the canonical replay/export artifact" for exactly this purpose. Theeditortool's create path converts LF to CRLF when, and only when, the operating system Cline ran on is Windows — and the log does not record which OS that was. A hash over the recorded content would be right on Linux and wrong on Windows, which is a hash agit did not compute over bytes it holds, so none is emitted. Edit results carry only a diff the runtime truncates at 200 lines. Everyeditorandapply_patchcall is counted in the import report as an edit agit cannot verify. - A Cline 3.x task directory imports, and its
<final_file_content>is not the file.agit import <globalStorage>/tasks/<taskId>(or theapi_conversation_history.jsoninside it) reads the layout every Cline release up to 3.89 wrote — frozen now that 4.0 moved on — the way that release's own source reads it: XML tool calls split by Cline's own parser and paired with the framed results that follow them, nativetool_usecalls by id, and, for tasks older than the transcript's owntsandmetricsstamps, dates and token counts taken from theui_messages.jsonbeside it by the index Cline writes on every timeline entry (or by request order where even that predates the file). Cline's own injected<environment_details>is counted, not filed as the person's words. The research on #63 expected this to be the layout where edits verify, since a write's result carries the whole file in<final_file_content>; readingDiffViewProvider.saveChangesshows it carries a normalized copy — line endings rewritten to the model's, trailing whitespace trimmed, one newline appended — so a hash over it is not a hash over the bytes on disk, and none is emitted. Roo Code's task directories are read by the same adapter, in Roo's dialect, from Roo's own source (archived May 2026): the same two files, but a UUID task id, Roo's tool and parameter names, an XML-era result written as two blocks (the[name for '…'] Result:frame, then the content), native results with no framing,toolErroras JSON by the end, condensed-context summaries and truncation markers in the transcript, and noconversationHistoryIndexever — so older Roo tasks pair with the timeline by request order, and usage is paired only when the request and turn counts agree. Which dialect a task is in is decided from the directory name (milliseconds are Cline's, a UUID is Roo's) or the transcript's own tells, recorded insession.start, and reported as runtimeclineorroo-code. Derived from the sources and checked against fixtures built to them, one per tool-call shape per dialect; a real task that disagrees names its unmapped blocks in the import report. - A Gemini CLI import is the recording as Gemini CLI itself would load
it.
agit import session-*.jsonlreads whatChatRecordingServicewrites: a metadata line, then messages appended again each time their tokens or tool calls land,$setupdates and$rewindTomarks. The adapter folds those by id exactly as the CLI's loader does, so a rewound turn is gone and a message's final form is what is recorded; the fold's losses (rewound messages, cancelled or unfinished tool calls,info/warning/errorlines) are counted by name. A live share holds the last message back until a newer one settles it, since the last message is the one the writer re-pushes; a rewind during a live share stops it as the rewrite of streamed history it is. Nofile.diff: an edit is a tool call whose result is prose, and no content is recorded beside it. The legacy single-document.jsonform is read too. Derived from the source and checked against a fixture built to it; a real recording that disagrees names its unmapped records in the import report. - A Kimi Code import reads the wire, not the context. Kimi keeps a
session's model context in
context.jsonl(no timestamps) and its event stream inwire.jsonl(each record stamped);agit import wire.jsonlreads the latter, folding it the way Kimi's own replay does — streamed text and thinking pieces merge into one assistant message per step, a tool call's argument parts fold into it, the step'stoken_usagebecomes itscost, and a/clearturn drops what came before. The session id is the directory the file sits in, as Kimi's docs say; the wire names no model and no working directory, so both are null rather than guessed. Interrupted and retried steps, notifications, approvals and every other wire message agit has no event for are counted by name, as is a diff display block — a viewer's excerpt of an edit, not the file — so there is nofile.diff. Derived from the source and checked against a fixture built to it; a real wire log that disagrees names its unmapped messages in the import report. - A pi import verifies writes outright and edits within a window.
agit import ~/.pi/agent/sessions/<project>/<session>.jsonlreads the formatsession-format.mddocuments: a header, then a tree of entries (/treebranches in place) linearized in file order with the tree kept undernative. pi'swritetool writes itscontentargument verbatim (fsWriteFile(path, content, "utf-8")), so a successful write is afile.diffhashed over exactly the bytes that reached the disk — recorded as a create when agit holds no prior content, since pi does not say whether the file existed, and counted as such. pi'seditrecords the unified patch it applied over the LF-normalized file; agit replays it the way edit.ts does — BOM off, line ending noted, normalize, apply, restore — when it already holds the file (a write, a verified edit, an untruncatedread, which returns the text unchanged, or--base), and counts the edit otherwise. A complete read is also tagged on its result, soagit forkcan rebuild a file whose first recorded edit came after one — the read has to hash to the edit'sbeforeHashto count. Shell edits stay invisible.usage.costis dollars and is counted, not stored. OpenClaw writes this same format, being built on pi; the two are told apart by session version (OpenClaw's is 4, pi's 3) and, for a version-3 file, by whose tools it calls, before either adapter claims it. Derived from the source and checked against a fixture built to it; a real session that disagrees names its unmapped entries in the import report. - A Hermes import reads
state.db, and verifies what Hermes itself verified.agit import ~/.hermes/state.dbreads thesessions,messagesandsession_model_usagetables from Hermes's own DDL, one session per import (--thread <id>picks one; every session with messages otherwise). Messages are in the OpenAI shape — tool calls as a JSON list on the assistant row, results astoolrows whose content is the tool's JSON, carried asstructured. Hermes'swrite_filepreserves a target's CRLF and BOM, then checks the disk's sha256 and reportsverifiedandbytes_written; when the count equals the argument's UTF-8 length nothing was transformed and the event's hash is over bytes Hermes confirmed — a create with a nullbeforeHashwhen agit held nothing before, since Hermes does not say whether the file existed. Itspatch(replace mode) answers with a difflib diff of the BOM-stripped file, which agit applies to content it holds. A V4A patch and aread_file(returned with line gutters, long lines clamped) are not replayed. Hermes keeps no per-message usage: each model's session totals become one aggregatecostat the session's end, flagged as such. Rows retired by compression or a rewind are kept and flagged.state.dbruns in WAL mode and Hermes keeps the WAL open, so the newest rows sit instate.db-wal; the import folds that sidecar in (below), so a running Hermes reads as it stands. Derived from the source and checked against a fixture written with the same DDL; a real database that disagrees names its unmapped rows in the import report. - A database still being written imports as it stands. SQLite in WAL
mode keeps committed pages in
<file>-waluntil a checkpoint, and every database agit reads (LangGraph, OpenClaw, OpenCode, Hermes) runs that way, so a reader that ignored the sidecar saw whatever was last checkpointed — agit used to refuse such a file. It now folds the sidecar's committed frames over the main file the way SQLite's own reader does: header checked, salts and the checksum chain verified frame by frame, the log ended at the first frame that breaks it, uncommitted frames left out, the latest committed page winning. The import report says how many frames in how many commits were folded in. A sidecar that is not a WAL at all is still refused, naming the checkpoint command. - An OpenCode import has no file history either.
agit import opencode.dbreads thesession,messageandparttables OpenCode writes (its drizzle schema and generated DDL, its v1 session schema for the JSON indata), in the order its ownMessageV2.pagereads them. Messages, reasoning, tool calls with their results, and onecostper model step (fromstep-finishparts, or from the message when there are none) all come through. OpenCode's file changes live in git snapshots of the worktree, not in the database, so apatchpart names files and carries no content: nothing is hashed, nothing is invented, andblame/fork/merge/diffhave nothing to work with. Every part type agit has no event for is counted by name. Derived from the schema and checked against a fixture built to it; a realopencode.dbthat disagrees names its unmapped parts in the import report. - A LangGraph import is the thread's current history, and no more.
agit import <checkpoints.sqlite>reads whatlanggraph-checkpoint-sqlitewrote — the SQLite file itself, with a zero-dependency reader for the file format and for the msgpack the checkpointer serializes with — and maps themessageschannel (LangChain messages: human, AI with tool calls and usage, tool results) onto the event log. What it follows is the parent chain from the checkpoint the runtime itself treats as current; a branch abandoned byupdate_state, a subgraph's own checkpoints, and any other thread in the file are counted, not imported. LangGraph has no file-edit construct, so there is nofile.diffand nothing forblame,fork,mergeordiffto work with; a message has no timestamp of its own and is dated by the checkpoint that first held it, which the report says. A database still open in WAL mode keeps its newest pages in the-walsidecar; agit refuses such a file rather than read a stale one as current. Graphs whose state has nomessageschannel, the betaDeltaChannelsnapshot form, andpicklecheckpoints are refused or counted by name. - A signature does not prove when, and does not prove truth.
agit signbinds a head to a key, which is what the chain alone could never do. The timestamp inside it is signed, so it cannot be edited afterwards, but it is still the signer's own claim — turning that into evidence needs a third-party RFC 3161 time-stamp, which agit does not issue. And a signature over a log full of false statements is a signed log full of false statements: it binds an identity to bytes, nothing more. - The MCP server labels untrusted content; it does not sandbox it.
agit mcphands recorded session text to a model, and that text can contain anything the agent saw, including material written to read as instructions. Every payload is framed as recorded data in a field no session can shadow, which is a label of the same kind as redaction and deserves the same scepticism. - The OpenTelemetry export tracks a moving target. The GenAI semantic
conventions are at Development stability, moved repositories during 2026,
and have never cut a release — so
--otelemits the only schema URL that exists, which ends in-dev, and will need updating as the conventions settle. - Redaction is a seatbelt, not a guarantee. Session logs contain whatever the agent saw. Before sharing one anywhere, read it.
Session logs are untrusted input: they may contain adversarial content and
are never executed, only displayed — the share page builds its DOM from
textContent exclusively and ships a CSP that forbids external resources.
Known credential patterns are redacted before events leave your machine
(at import and during live shares alike) and counted in meta.json. Share
links are unguessable 128-bit capabilities with TTLs; the relay holds
everything in memory, binds loopback by default, and persists nothing. A
steer key is a second, separate capability: the link lets someone read, the
key lets them queue a message for the agent, and the relay never learns
whether a key was right — only the sharer holds it. Never commit real
session logs to this repo — tests run against synthetic fixtures.
Node 20+, TypeScript, ESM, zero runtime dependencies.
npm install
npm run build # tsc -> dist/
npm test # vitest
Conventional Commits, small and focused. If a real session breaks an adapter, fix the adapter, not the fixture. CONTRIBUTING.md has the full onboarding path — repo map, adapter-writing guide, and the rules that are not suggestions. CI runs build + tests on Linux and Windows, Node 20/22.
Apache-2.0
