Blocking tool commands with hooks gives you fine-grained control over what Claude Code can execute. Use PreToolUse to intercept commands like echo, validate tool usage, and enforce conventions before any tool runs.
Exit code 2 (simple):
stderrJSON output (structured):
permissionDecision: "deny"permissionDecisionReasonStop commands with a simple error:
import { type PreToolUseHookInput } from "@anthropic-ai/claude-agent-sdk"
const input = await Bun.stdin.json() as PreToolUseHookInput
type BashToolInput = {
command: string
description: string
}
if (input.tool_name === "Bash") {
const toolInput = input.tool_input as BashToolInput
if (toolInput.command.startsWith("echo")) {
console.error("echo is not allowed")
process.exit(2)
}
}
// No exit or exit 0 = tool continues
When Claude tries to use echo, the hook blocks it and shows the error message. This approach works but doesn't guide Claude toward the correct alternative—that's where JSON output helps (see lesson 16).
Update .claude/settings.local.json:
{
"hooks": {
"PreToolUse": [{
"matcher": "",
"hooks": [{
"type": "command",
"command": "bun run .claude/hooks/PreToolUse.ts"
}]
}]
}
}
Match specific tools with the matcher field:
Bash - Shell commandsWrite - File creationEdit|MultiEdit - File edits (regex pattern)Read - File reading* or "" - All toolsPrompts:
use echo to echo hello world
write a hello world file to the root of this project
[00:00] So we'll clear out our logic for user prompt submit and now turn our attention to a pre tool use hook and we'll let cursor fill this out with a tab. We'll leave our matcher blank so we capture all of them and then bun run a pre tool use script. So for our clod hooks pre-tool use script we're now going to import the pre-tool use hook input type and then cast our input to this new pre-tool use hook input. Now I have to call out that while I've been recording these lessons they renamed the SDK from clod-code to clod-agent-sdk. Just one of those things.
[00:40] And we'll navigate into our clod directory, into the hooks, and say bun-clod-code and instead clod-agent-sdk. Now all of the types are essentially the same. They did add some more features which we'll get into, but for now we just have to rename these. And then we can uninstall the clod code SDK. So bun remove.
[01:03] So we should be back in business here. So close this out, go back over to our pretool use, and we're simply just going to write out a file to capture what the input actually is. Launch Cloud Code. You'll see they also updated to Cloud Code 2.0. And you'll see the cute little mascot.
[01:21] They named it Clawd. C-L-A-W-D. And we'll just say use echo to echo hello world. Hit enter. And then if we hit ctrl-o to inspect this you'll see it invoked what they call the bash tool, and you'll see the syntax here in parens where the command and the arguments are wrapped inside of the bash tool.
[01:41] You'll see the result of hello world and you'll see that it ran our pre-tool use with bun, run, and where our script is. So if we check the result of this file in our hooks directory, pre-tool use input, you'll see that, close this out too, we have a tool name of bash which lines up with the tool name and the tool input with the command and description, the command being echo including the arguments. And these slashes just have to escape this so that it's formatted well in JSON. So if we want to prevent something like an echo we can check the tool input to check the command to see if it starts with echo. Now the way they have the types right now if you check the input.toolInput() if I try and check the properties here it's not going to list it because tool input is currently typed as unknown.
[02:32] And that's because based on the type of tool it runs, whether it's bash or the write tool or the edit tool or the other built-in tools, like the write tool would have the file path and the content to write to the file. But right now we're just working with bash. So we're going to say if the input tool name equals bash, it will create a type of bash tool input. And as a general cursor tip, if you have a file open next to your file it makes it much easier to tab auto-complete if you're trying to infer things from this other file. So we'll check on here by grabbing the tool input, casting it as the bash tool input, and then if the tool input command includes echo or starts with echo, We can reject this with process exit2 and give a console error of like echo is not allowed.
[03:25] So now if we close this out, start up Cloud, and run the exact same command we'll just press up to go back in our command history, hit enter. You'll see that this time we run into a bash operation is blocked by the hook and echo is not allowed. So depending on the bash tools you'd want to block you'd put them inside of here. And while this hook will still run on every other pre-tool. So if we say write a hello world file to the root of this project and we'll allow it to write, you'll see if we press ctrl-o it invoked the write tool, it invoked our pre-tool use hook, but it completed successfully because this tool name is named right and since it didn't match bash it just continues and exit successfully.