what you're trying to do
capture should not run in real time by default. today every adapter ships a
PostToolUse hook that shells out to vouch capture observe after every
tool call, plus a Stop hook that shells out to vouch capture answer after
every turn. on a busy session that's hundreds of python process spawns, each
one loading the kb store, to append a line to an ephemeral buffer that only
matters once — at SessionEnd.
the knowledge itself is already batched: capture.answer_mode defaults to
session (capture.py:45), so claims are
extracted once at finalize from the full transcript, and the per-turn Stop
hook is a pure deferral — it fires, decides it is in session mode, and exits.
the real-time half of capture is doing work whose only consumer is a
once-per-session rollup.
there is also a correctness angle. the buffer is per-tool-call hearsay: a
Command failed: line, a truncated grep -rn "delete" src/vouch/*.py | head.
the transcript that finalize already reads is strictly richer and is the
receipt-bearing artifact. two sources of truth for "what happened in this
session", one of them lossy.
what you've tried
capture.enabled: false is the only existing knob and it is all-or-nothing —
it turns off the session summary too, which is the part worth keeping. there is
no way to say "capture, but only at the end".
suggested shape
- new config key
capture.realtime, default false. when off,
capture observe returns a {"skipped": "realtime-disabled"} no-op
immediately, before touching the store.
- drop the
PostToolUse block from the shipped adapter templates
(adapters/claude-code/.claude/settings.json and the equivalent in the
other host adapters), and drop Stop → capture answer while we're there
— under the default answer_mode: session it can never file anything.
SessionStart → finalize-all, UserPromptSubmit → context-hook, and
SessionEnd → finalize are the three that carry weight.
- reconstruct tool activity from the transcript inside
finalize instead of
from the buffer, so the summary body keeps its "files modified / activity /
notable commands" sections without the per-call hook.
capture.realtime: true restores today's behaviour for anyone who wants
the buffer as a crash-resistant backstop.
compatibility considerations
this is the part that needs care. session_split.summarize gates on
len(observations) + len(changed_files) < cfg.min_observations (default 3,
session_split.py:131). with the buffer
empty, only the git-diff backstop counts — so any session touching fewer than
3 files would file no summary at all. item 3 above is therefore not
optional; it is what keeps the gate meaningful. the alternative is counting
transcript exchanges toward total.
existing kbs are unaffected — no stored artifact changes shape. adapter
templates are regenerated by install_adapter, so installed hosts keep their
current settings.json until they re-run the installer; worth a line in the
changelog either way.
not a surface change, so no VEP: capture observe stays on the method list
and stays callable, it just defaults to a no-op.
alternatives
- keep the hook but make it write nothing when
answer_mode: session — same
saving in effect, but leaves the confusing "hook fires, does nothing"
shape and still pays the process-spawn cost.
- debounce
observe (write at most every N seconds). cheaper to build, but
keeps two sources of truth and still spawns a process per tool call to
decide it should not write.
what you're trying to do
capture should not run in real time by default. today every adapter ships a
PostToolUsehook that shells out tovouch capture observeafter everytool call, plus a
Stophook that shells out tovouch capture answerafterevery turn. on a busy session that's hundreds of python process spawns, each
one loading the kb store, to append a line to an ephemeral buffer that only
matters once — at
SessionEnd.the knowledge itself is already batched:
capture.answer_modedefaults tosession(capture.py:45), so claims areextracted once at finalize from the full transcript, and the per-turn
Stophook is a pure deferral — it fires, decides it is in session mode, and exits.
the real-time half of capture is doing work whose only consumer is a
once-per-session rollup.
there is also a correctness angle. the buffer is per-tool-call hearsay: a
Command failed:line, a truncatedgrep -rn "delete" src/vouch/*.py | head.the transcript that
finalizealready reads is strictly richer and is thereceipt-bearing artifact. two sources of truth for "what happened in this
session", one of them lossy.
what you've tried
capture.enabled: falseis the only existing knob and it is all-or-nothing —it turns off the session summary too, which is the part worth keeping. there is
no way to say "capture, but only at the end".
suggested shape
capture.realtime, defaultfalse. when off,capture observereturns a{"skipped": "realtime-disabled"}no-opimmediately, before touching the store.
PostToolUseblock from the shipped adapter templates(
adapters/claude-code/.claude/settings.jsonand the equivalent in theother host adapters), and drop
Stop→capture answerwhile we're there— under the default
answer_mode: sessionit can never file anything.SessionStart→finalize-all,UserPromptSubmit→context-hook, andSessionEnd→finalizeare the three that carry weight.finalizeinstead offrom the buffer, so the summary body keeps its "files modified / activity /
notable commands" sections without the per-call hook.
capture.realtime: truerestores today's behaviour for anyone who wantsthe buffer as a crash-resistant backstop.
compatibility considerations
this is the part that needs care.
session_split.summarizegates onlen(observations) + len(changed_files) < cfg.min_observations(default 3,session_split.py:131). with the buffer
empty, only the git-diff backstop counts — so any session touching fewer than
3 files would file no summary at all. item 3 above is therefore not
optional; it is what keeps the gate meaningful. the alternative is counting
transcript exchanges toward
total.existing kbs are unaffected — no stored artifact changes shape. adapter
templates are regenerated by
install_adapter, so installed hosts keep theircurrent
settings.jsonuntil they re-run the installer; worth a line in thechangelog either way.
not a surface change, so no VEP:
capture observestays on the method listand stays callable, it just defaults to a no-op.
alternatives
answer_mode: session— samesaving in effect, but leaves the confusing "hook fires, does nothing"
shape and still pays the process-spawn cost.
observe(write at most every N seconds). cheaper to build, butkeeps two sources of truth and still spawns a process per tool call to
decide it should not write.