Skip to content

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.