agit
Health Warn
- License — License: Apache-2.0
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Low visibility — Only 5 GitHub stars
Code Fail
- network request — Outbound network request in fixtures/cline-sdk/simple.messages.json
- rm -rf — Recursive force deletion command in fixtures/gemini-cli/generate/session.py
- rm -rf — Recursive force deletion command in fixtures/gemini-cli/legacy.json
Permissions Pass
- Permissions — No dangerous permissions requested
No AI report is available for this listing yet.
git for running agents — inspect, replay, share, fork AI coding sessions
agit — git for running agents
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
What works today
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;--latest
takes 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-file
does 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,fork
andprall 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.completed
yet — 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.additionalContext
channel, 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,chat
children 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 canagit 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, andverify
catches 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 reachesimport,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.
Adapter matrix
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.
The format
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.
What does not work yet
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 theapply_patchtext it records, replayed with OpenClaw's own
matching rules. pi'swritewrites its argument verbatim and itsedit
records the patch it applied, so both verify — a write against the bytes
it wrote, an edit against content agit already holds. Hermes'swrite_file
verifies 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 usesgit 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. Theeditor
tool'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_use
calls by id, and, for tasks older than the transcript's owntsandmetricsstamps, dates and token counts taken from theui_messages.json
beside 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 whatChatRecordingService
writes: 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 incontext.jsonl(no timestamps) and its event
stream inwire.jsonl(each record stamped);agit import wire.jsonl
reads 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-sqlite
wrote — 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-wal
sidecar; 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.
Security posture
Session logs are untrusted input: they may contain adversarial content and
are never executed, only displayed — the share page builds its DOM fromtextContent 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.
Development
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.
License
Apache-2.0
Reviews (0)
Sign in to leave a review.
Leave a reviewNo results found