Welcome to the show -- James, picture this: your Bash tool finishes,
and 18, 342 milliseconds later a message pops into Slack saying,
"Bash tool completed in 18342ms," straight from .claude/settings.
json.
No wrapper script.
No glue code.
Just the hook talking DIRECTLY to an MCP tool.Wait
-- 18, 342 milliseconds is weirdly specific,
which is why I love it.
You're saying in Claude Code 2.
1.118,
a hook can use type: "mcp_tool" itself?
Like the hook is the MCP client now,
not some bash middleman pretending to be one?Yeah,
exactly.
And that's the sneaky-big shift.
In 2.
1.118, hooks got this direct path: you can set type to "mcp_tool",
and then a PostToolUse hook can call something like send_message on a
Slack MCP server.
Before that, the setup was a bit how ya goin' -- the hook fired a shell
script, the shell script then invoked the MCP tool,
and suddenly you've got extra config,
separate auth handling, and this leaky abstraction where the "automation"
is really just duct tape in a trench coat.The "separate auth
handling" part is the one that jumps out.
Because if the bash script has to know how to reach Slack,
or GitHub, or whatever, that means credentials,
environment differences, quoting bugs...
all the gross stuff.
It stops feeling like one system and starts feeling like three systems
stacked on top of each other.
Right.
And if you've ever babysat one of those scripts at midnight -- I have,
different context, same pain -- it's always the silly bit that breaks.
A path changes.
A token isn't available in one shell.
A JSON payload gets mangled because you forgot how many quotes bash wants
today.
With the direct MCP tool call,
the hook config lives where the behavior lives.
That's the cleaner mental model. Walk me through the concrete
flow, though.
I want the exact chain.
Tool runs, hook sees it, then what?Okay.
Say a Bash tool finishes.
That emits a PostToolUse event.
In .claude/settings.
json, you've got a hook configured for that event,
and instead of launching a script,
the hook uses type: "mcp_tool".
The payload templates in {{duration_ms}} -- we'll get to why that field
matters in a sec -- and then it calls,
say, send_message on your Slack MCP server.
The message body is literally "Bash tool completed in 18342ms," and the
target channel is #dev-activity.So
#dev-activity gets
that post as a side effect of the tool event itself.
Not the agent deciding in natural language,
"hey maybe I should tell Slack." Not a shell script doing an impersonation
of integration logic.
The hook is just wired to the event.
Spot on.
And that distinction matters more than it sounds.
Because "Claude decided to say something" is fuzzy.
"This event triggers this MCP tool call" is deterministic.
That's automation.
It's less like asking a coworker to remember to text you and more like
wiring a relay in the shed -- ugly analogy,
but once the circuit closes, the light comes on.
Every time.No, that's a good one.
And I think the surprise here is people will hear "convenience feature"
and think, cool, fewer lines of config.
But the real change is architectural.
Hooks used to be scripts that react to events.
Now they're first-class automation nodes that can talk to Slack,
Linear, GitHub -- any connected MCP server.Yep.
That's the bit I'd underline in red.
If a hook can directly call an MCP tool,
then the hook is no longer just a local afterthought.
It's participating in the same tool ecosystem as the agent.
That means a completed edit could log to GitHub,
a failed command could open or update a Linear issue,
and a long-running task could ping Slack without this weird shell-wrapper
hop in between.
And because it's all in .claude/settings.
json,
the behavior is visible.
That's another underrated thing.
When the logic lives in random scripts sprinkled around a repo,
nobody knows what's actually happening.
When it lives in one config file,
at least you can audit it. Totally.
The old pattern made hooks feel bolted on.
Useful, sure, but bolted on.
This makes them feel native.
And once something feels native,
people stop using it just for notifications and start using it for
process.
That's where it gets interesting...
and a tiny bit spicy.Yeah,
because 2.
1.119 adds the other half of this.
PostToolUse and PostToolUseFailure hook inputs now include duration_ms.
And the definition is SUPER clean: it measures only the tool's execution
time.
Not permission prompts, not PreToolUse processing,
not Claude's internal decision time.
Just the tool run itself.Let me try to say that back.
If Bash takes 31 seconds to actually execute,
but there was some faffing about before that -- permissions,
thinking, pre-checks -- duration_ms ignores the faffing and clocks the
31 seconds?
Exactly.
The "31 seconds" token is the one that matters.
It's basically a pure I/O signal.
So if you want a rule like "alert only if Bash takes longer than 30 seconds,"
duration_ms is useful because it's not polluted by everything else happening
around the tool.
Same for "skip logging sub-200ms edits." Two hundred milliseconds means
the edit itself was fast, not that the whole agent experience felt fast.
That
"sub-200ms edits" example is gold,
actually.
Because if you log EVERY Write or Edit call,
you get a firehose.
Proper garden-hose-to-the-face stuff.
But if you say, only log Write or Edit when duration_ms crosses some
threshold, now the noise drops and the signal pops.
Right -- and that's where matchers matter.
The hook config can combine event matching with performance thresholds.
So you can target only Write calls,
or only Edit calls, or only failures through PostToolUseFailure,
and then route only those events to an MCP tool for logging or notification.
And
because failures have duration_ms too,
you can start asking more useful questions.
Did the failure happen instantly,
like a bad invocation?
Or did it fail after 45 seconds,
which smells more like a timeout or external dependency issue?
Same failure event, very different story.Though this is where
I wanna push on the "workflow control layer" idea.
Notifications are safe-ish.
But once you have direct MCP calls plus timing,
it's tempting to build reactive systems: if Bash exceeds 30 seconds,
send a Slack message; if Edit fails twice,
create a ticket; if Write takes too long,
maybe trigger some follow-up action.
And now your hook system isn't just watching work -- it's steering
work.
I think that's true, and I think it's the whole point.
But yeah, there's a trap there.
If the hook's MCP call is just a side effect -- post to Slack,
log somewhere, maybe tag a Linear issue -- sweet as.
If the hook kicks off something that causes MORE agent activity,
you can end up with a feedback loop.
Tool finishes, hook fires, MCP action triggers new activity,
new activity triggers another hook...
and suddenly even a kangaroo could trip over that fresh code.
The feedback loop is the scary part.
Especially because it won't look scary at first.
It'll look elegant.
"Oh cool, on failure we auto-create something." Then that something pings
another system, that system updates state,
the agent sees state change, and now you've built the software equivalent
of a microphone pointed at a speaker.
Exactly -- the awful squeal.
And the clean timing signal makes the automation more tempting,
not less, because now the rules can be smart.
Longer than 30 seconds?
Alert.
Shorter than 200ms?
Ignore.
Failure plus long duration?
Escalate.
That's proper operational logic.
So the upgrade is small on paper -- a new type in 2.
1.118,
a new field in 2.
1.119 -- but together they move hooks from passive event
listeners into something closer to a policy layer.Yeah.
It's the combination that matters.
Direct MCP calls without duration_ms are useful but kind of blunt.
duration_ms without direct MCP calls is informative but still clunky.
Put them together, and a hook can say: for this exact tool,
under this exact condition, call this exact external system.
That's not "just notify me." That's workflow design.
And maybe the best
version of that is boring on purpose.
Use the timing to cut noise.
Use the MCP call for side effects that humans actually need.
Don't let the hook become a tiny chaos goblin running your whole stack.
If your Slack channel learns every 18342ms Bash run but your
agent starts chasing its own tail,
you've solved the wrong problem. That's the question I'd leave
people with: when your hooks can see how long work took and can talk directly
to the rest of your tools, are you building observability...
or are you quietly building a second automation system inside the first
one?Bit of both, probably.
Catch you next time.