Claude Code Hooks Now Call MCP Tools Directly
This episode explores how Claude Code 2.1.118 and 2.1.119 turn hooks into first-class automation, letting PostToolUse events call MCP tools like Slack without wrapper scripts or brittle bash glue. It also digs into duration_ms as a clean timing signal for smarter logging, alerts, and workflow routing.
This show was created with Jellypod, the AI Podcast Studio. Create your own podcast with Jellypod today.
Get StartedIs this your podcast and want to remove this banner? Click here.
Chapter 1
Hooks become real MCP clients
Lachlan Reed
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. [pause] No wrapper script. No glue code. Just the hook talking DIRECTLY to an MCP tool.
James Turner
[responds quickly] 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?
Lachlan Reed
[excited] 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.
James Turner
[skeptical] 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.
Lachlan Reed
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. [reflective] With the direct MCP tool call, the hook config lives where the behavior lives. That's the cleaner mental model.
James Turner
[curious] Walk me through the concrete flow, though. I want the exact chain. Tool runs, hook sees it, then what?
Lachlan Reed
[matter-of-fact] 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.
James Turner
[questioning tone] 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.
Lachlan Reed
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.
James Turner
[laughs] 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.
Lachlan Reed
[warmly] 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.
James Turner
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.
Lachlan Reed
[reflective] 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.
Chapter 2
duration_ms turns hooks into a workflow layer
James Turner
[excited] 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.
Lachlan Reed
[questioning tone] 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?
James Turner
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.
Lachlan Reed
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.
James Turner
[responds quickly] 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.
Lachlan Reed
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.
James Turner
[skeptical] 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.
Lachlan Reed
[pauses] 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.
James Turner
[laughs] 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.
Lachlan Reed
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.
James Turner
[reflective] 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.
Lachlan Reed
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. [chuckles] If your Slack channel learns every 18342ms Bash run but your agent starts chasing its own tail, you've solved the wrong problem.
James Turner
[softly] 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?
Lachlan Reed
[warmly] Bit of both, probably. Catch you next time.
