Most systems bolt an audit log onto the side: a table of “user X edited doc Y at time T.” It works until the log and the data disagree — a write that skipped the logger, a backfill that lied, a replay that lost an actor. OpenKnowledge sidesteps the whole class of bug by making git itself the audit trail.
Every edit — human or agent — is committed into a shadow repository at .git/ok/, and each writer gets its own ref: refs/wip/main/{writerId}. On quiescence, persistence builds one tree and writes one commit per active writer (packages/server/src/shadow-repo.ts:227-323), authored as that writer:
GIT_AUTHOR_NAME = "claude-session-123"
GIT_AUTHOR_EMAIL = {writerId}@openknowledge.local
The commit message then carries a JSON footer — an OkActorEntry (packages/server/src/persistence.ts:406-482) — recording writer_id, principal, agent_session, agent_type, client_name, client_version, the docs touched, and per-doc summaries.
Read that with an audit hat on: for any change to any document you can recover which agent type, which session, which tool version made it, what it touched, and why — and you can’t get it wrong, because it’s the same commit the content lives in.
Why this is non-obvious
The instinct is one shared history plus a log. But a shared branch forces a serialization order on concurrent writers and tangles attribution into merge commits. Per-writer refs invert it: each agent and human is an independent commit stream you can diff, blame, and merge separately. The log isn’t near the data — it is the data.
The pitfall
Per-writer refs multiply fast (one ref per agent session). You need a promotion/GC story — OpenKnowledge’s GitHub sync collapses WIP refs into the real branch — or .git/ok/ grows without bound.
Sources
-
open-knowledge/07_git_sync_audit.md? unverified -
packages/server/src/persistence.ts:406? unverified -
packages/server/src/shadow-repo.ts:227? unverified