A few weeks ago I published dlog, a tiny append-only decision log for coding-agent sessions: dlog add "chose X" -r "because Y", one JSONL line per decision.
The best feedback came from reader loganilands, across four rounds of discussion. They'd run honesty experiments on frontier models: asked plainly, models answered "I don't know" correctly 0 out of 8 times; asked with leading questions, they confidently fabricated 8 out of 8. Their summary stuck with me: "Empty, verified, wrong."
Their point: read-time verification only protects what's already written down. The real loss happens at write time — the decision that never gets recorded at all. And they were right that the git-sha anchor had to be checked at read time, forcibly, not as an optional flag.
v0.2 ships both gates.
Gate 1 — read-time verify (enforced). dlog verify re-resolves every entry's git anchor against the current repo. An entry whose commit no longer resolves — tampered log line, rewritten history — doesn't get a red warning. It disappears. Fail-closed: if the tool can't confirm it, the tool doesn't show it.
$ dlog verify
10-11 08:30 chose sqlite over postgres [db, infra]
verify: 1 kept, 1 dropped (anchor commit not found)
Gate 2 — write-time gate (a protocol). dlog add now prints an entry id. dlog gate --check <id> exits 0 iff that decision was recorded, 2 otherwise. dlog assert-logged bulk-checks ids from args or stdin. The intended use is in the agent loop:
ID=$(dlog add "chose sqlite" -r "zero deps" | grep -oE '[0-9a-f]{12}' | tail -1)
# ... do the work ...
dlog gate --check "$ID" || exit 1 # refuse to act on what was never recorded
Now the honest part, because loganilands would call it out if I didn't:
-
verifyproves the anchor commit still exists. It does not prove the entry text is unchanged — JSONL is a plain file; text can be rewritten around a valid sha. Anchor tampering is the cheap, common attack; that's what we catch. - The write-time gate is a protocol, not physics. Nothing in dlog can force an agent to call
gate --check. It binds cooperative loops; it can't bind an agent that skips the call. I'm shipping the mechanism and the tests, not a claim of enforcement. - Entries recorded outside a git repo have no anchor;
verifyshows them as-is and says so.
"Empty, verified, wrong" is still possible — an empty log verifies trivially. The read-time gate is the one I can enforce; the write-time gate is the one that matters. Two gates, and I'm telling you which is which instead of pretending one mechanism covers both.
- GitHub: https://github.com/hahahahahahahahah6/decision-log
- PyPI: https://pypi.org/project/decision-log/
-
pip install decision-log— stdlib only, no dependencies.
Top comments (1)
Ran both gates against a scratch repo (Python 3.13,
dlog.py12335 chars at HEADb42b9029d667). Three readings that aren't in the "Honest limits" section, then the one that is.verifyexits 0 whether it keeps or drops. I corrupted two anchors:verifydropped both, printedverify: 0 kept, 2 dropped (anchor commit not found)to stderr, and returned 0. Sodlog verify || exit 1never fires — the read-time gate is enforced in what it prints, not in what it exits, and$?is the signal a loop or CI actually branches on. The write-time gate does exit 2. The asymmetry runs backwards from the naming: the gate you call "enforced" is the one with no failure signal.When every entry is dropped, stdout is the empty-log message. All anchors bad -> stdout is
no decisions logged yet — use `dlog add "title" -r "reason"` to record one. A fully-failed verify is byte-identical on stdout to a log that was never written; only the stderr summary separates them. That's the read-time mirror of your own "empty, verified, wrong": tampered reads as empty.The parenthetical fires unconditionally. Two clean entries, nothing dropped ->
verify: 2 kept, 0 dropped (anchor commit not found). The cause is named even when nothing failed, which is how a line teaches readers to skip it.Confirming the limit you do document: I rewrote
title+reasonand kept the valid sha -> still kept and shown. If you'd ratherverifycatch that than rely on committing the log, stamp a content hash at add and check it in verify:Measured on the same tree: honest entries still show (2 kept, 0 dropped); a text-only rewrite now drops (1 kept, 1 dropped). Six lines, no new deps, and it works whether or not the log is committed.
The "read-time is the one you can enforce, write-time is the one that matters" split is the right axis — it's just that the exit code doesn't carry the read-time verdict yet.