DEV Community

Harris Ahmad
Harris Ahmad

Posted on

MCP tools are model-elective. Hooks aren't. That distinction turned out to matter more than I expected.

An MCP server can expose the perfect tool and the model can simply not call it. Nothing is wrong, nothing errors — the information just doesn't arrive. Hooks are the opposite: the harness runs them, so whatever they return lands in context whether the model asked or not.

I got stuck on this while trying to solve a specific problem: I'd change a base image or a Terraform module in one repo, and only find out later that three other repos pinned the same thing. The knowledge existed, it just wasn't anywhere the agent would look.

So I built the thing as a hook rather than a tool. A PreToolUse hook fires when a manifest is read and pushes cross-repo impact in unprompted; a Stop hook captures what the session learned. Extraction stays the model's job — it handles multi-stage aliases, ARG-templated bases and FROM inside a heredoc that my regex attempts got wrong.

What I can actually back up, measured against a fixture corpus with 19 deliberate traps planted in it:

39/39 artifacts extracted, 39/39 version specs intact (a model that silently normalises ^18.2.0 to 18.2.0 destroys the whole signal)
43 OSV advisories matched, 9 that actually reach the versions in your lockfiles
~40 tokens per injection, ~126 always-on

Honest caveats: that corpus is synthetic and I wrote it, so it's a floor not a proof. Nobody but me has run this yet. npm cross-repo impact is weaker than infrastructure, because packages install independently per repo. pnpm lockfiles aren't read.

pip install blastradius-mcp — MIT, local-only, one SQLite file, nothing leaves your machine.

GitHub Link

Mostly I'm curious whether the hooks-over-tools framing holds up for other people's use cases, or whether I've over-fit it to mine.

Top comments (1)

Collapse
 
skillselion profile image
Skillselion

To answer your closing question: the framing holds, with one asymmetry worth naming. "An MCP server can expose the perfect tool and the model can simply not call it" is true, and the hooks docs back the other side: additionalContext from a PreToolUse hook is wrapped in a system reminder and read on the next model request, no election involved.

The asymmetry is failure mode. A tool that is never called leaves a visible gap in the transcript. A hook fails open: per the docs, on PreToolUse only exit code 2 blocks, a crash that exits 1 with no JSON on stdout is a non-blocking error and the call proceeds, and a timed-out command hook does not block the call either. So a crashed impact hook and "no cross-repo impact found" look identical from inside the session. We would add a heartbeat line ("blastradius checked N manifests") so silence is distinguishable from a green result.

On the Stop hook: the docs note transcript_path can lag the in-memory conversation and recommend last_assistant_message for the final turn. Is your extraction reading the transcript file, and has a late write ever dropped the last learning?