v5.18.0
Hooks can run on the MCP server the host already holds open, and file evidence can finally
go stale.
File evidence can go stale, which the README has promised since it was written.
isEvidenceStale compares a file's current hash against the one the evidence recorded, and no
shipped writer ever recorded one: affectedPaths became file evidence with contentHash NULL, the
one module that hashed files has no importer, and the MCP schema has no field for a caller to
supply one. Symbol evidence went stale; file evidence could not. The obvious fix — hash every cited
path — would have turned an agent's unverified assertion about a file into a staleness claim about
it, and that gate was real. The read-set is now the gate: a cited path some session provably
opened (work_read_sets holds a file:// or symbol:// row for it, captured from the tool
stream and never from the agent's own report) is hashed from disk at write time; a path merely
declared stays unhashed and never reports stale. One disk read per observed path per write, on the
write path only. session-evidence.ts is untouched and still unwired; whether to delete it is a
separate call (#225).
"Two independent confirmations" now means two days, and the items that had already earned it
are promoted. The comment above VERIFY_THRESHOLD promised independent confirmations; the query
counted rows, so two knowl_feedback calls in one turn — one agent, one item, one source — promoted
an item to verified. Measured on the project's own store, every item the row count would have
promoted was a burst inside a single session: four useful events in seven minutes on one, two four
minutes apart on another. The feedback path now counts distinct days, the unit the observed-use path
already uses, and neither clears the bar. The second half is the one that matters more: promotion
was edge-triggered only, run at the instant a feedback row was written and never again, so an item
whose confirmations crossed the bar before knowl_feedback was wired to standing stayed asserted
forever — the store holds one with three useful events against a threshold of two. Session start
now re-evaluates the feedback predicate over every asserted item, capped and audited the way the
observed-use pass is, so the backlog drains instead of stranding (#223).
Hooks can run on the server the host already holds open, instead of as a process per event.
Every Knowl hook has been a command hook: a fresh knowl agent-hook process per event, measured
at ~230ms of Node startup each and paid twice per tool call, serialized against the agent's own
work because the host waits on the pre-tool hook. Over 102 real Claude Code sessions that is 31s
of startup at the median session and 190s at the 90th percentile — while the MCP server sat there
with the database open and the embedding model loaded. Claude Code 2.1.257 and Codex 0.148 can run
a hook as a call to a tool on a connected MCP server, so hooks.transport: mcp now writes the
mid-session events as mcp_tool hooks calling knowl_hook and registers that tool; SessionStart
stays a process because both hosts say it fires before servers finish connecting. Opt-in and
command by default, because moving costs a catalog entry — MCP has no hidden-tool concept, and a
34th tool against a surface already measured at ~10.5K tokens is paid only by the repositories that
asked for it. The payload travels as ${field} templates the host fills in, is rebuilt on the
server whichever way the host rendered each one, and then goes through the same allowlist the
stdin path applies, so nothing reaches the handler by this route that the other would have dropped.
Calling the tool while the transport is command is refused rather than run, so a client holding
a stale tool list cannot capture every event twice. A write the gate refuses still says why on the
server's stderr, which is where the host writes its MCP log — the same second copy the process
path prints, and worth more here, because a block whose verdict this new transport got subtly
wrong would otherwise be completely silent (#224).