D353 — write findings reach the agent through a client-generated hook¶
Decision. A write response's findings are returned to a Claude Code agent as
feedback by cartographer-write-findings, a second synthetic hook generated by the
client (the D60 pattern), not shipped by any KB. A PostToolUse entry matching the
seven write tools runs a shim that hands the payload to the internal subcommand
cartographer hook write-findings; with at least one finding, of any severity, it
writes a capped message to stderr and exits 2, otherwise it is silent. It is opted
out per machine with write_findings_hook: false in .cartographer.yaml.
Why. Findings are info, inside a JSON field of a successful result, and
agents, subagents above all, skip them. A hook is the only channel that does not
depend on the agent choosing to look. The logic lives in the binary because parsing
JSON in sh or cmd would need jq; the script stays a shim that turns every
outcome but exit 2 into 0, because a hook must never break a session (a Go panic
also exits 2, so the subcommand recovers). The hook cannot undo the write, so it
blocks on any severity and the loop ends when the agent fixes or lint_ignores.
Alternatives rejected.
- A bundled hook through the manifest: needs a new artifact source, a signing
exemption and per-KB opt-out handling, and makes a machine-wide behaviour depend
on which KBs a server mounts.
- A per-KB write_findings_hook key on the server: the hook lives in the
machine's client directory and fires for every KB it talks to; the key has no
channel to it.
- Other clients in the same plan: the payload, matcher and feedback semantics are
verified for Claude Code only; capabilities are declared, not inferred. Kiro's
PostToolUse was never probed. A follow-up issue per client probes each.
- Blocking only on warnings: most findings are info, which is exactly what is
being skipped.
Consequences. hookMechanism.writeFindingsHook is the per-provider declaration.
provisioning.WriteFindingsTools must list every tool whose handler returns
findings; a test in internal/mcpserver reads tools_write.go and fails when a new
one is missing. The payload shape is parsed leniently (result object, content-block
array, JSON in text, supersede's plain-text form), so a client change makes the hook
quiet rather than loud. A rejected write (the D350 gate) is an error result and is not
a PostToolUse success, so it never reaches the parser. The provisioned instructions
say a write is done when its response has no findings.