Demystifying Claude Code Hooks: A Peek Under the Hood
AI can make development feel less transparent. Let's look under the hood at how Claude Code hooks work and how to run arbitrary code when tools initialize.

As a developer with years of experience, I've always valued transparency. Before the AI boom, I could always prove to myself exactly what my code was doing and why. But with tools like Claude, things can sometimes feel like a black box. You ask it to do something, it does it, but who's to say it's not just hallucinating or "pretending" to give you what you want?
That's exactly why I like to dig under the hood. Sure, Claude can write a lot of this configuration for you if you just ask, but understanding the actual mechanics gives you real control. Today, I'm sharing how Claude Code hooks work, how they interact with your local environment, and how you can use them to run arbitrary code.
The Foundation: claude.md
Before we get into hooks, we need to understand how Claude knows what it's looking at. If you've used Claude Code, you might be familiar with the /init command. When you run this, Claude reads your entire repository, understands its purpose, and generates a claude.md file at the root of your project.
This markdown file is essentially a cheat sheet for Claude. It contains the basic context of your project—what it is, where you are, and how to run it (for example, instructions to run node qr.js). Once this file exists, every single message you send to Claude automatically includes claude.md in its context. It grounds the AI in your specific project environment.
Peeking into the .claude Folder
Alongside the markdown file, initializing Claude Code also sets up a hidden .claude folder. This directory is where the real magic happens. It's the home for custom skills, custom commands, and what we are focusing on today: hooks.
Hooks allow you to execute arbitrary code whenever Claude Code initializes a specific tool. If you type the /hooks command in your terminal, Claude will output a list of available hooks and show you which ones currently have tools attached to them.
Configuring Hooks in settings.json
To see this in action, we look inside the settings.json file located in the .claude folder. Let's say we have a pre-tool use hook configured.
In the settings, you'll see a matcher property. If the matcher is set to *, the hook fires for every command Claude executes. But we can get specific. If you ask Claude to list out all its available tools (in bullet points, of course), you'll see tools like read. If we change our matcher to "read", our hook will only trigger when Claude attempts to read a file.
One crucial detail to remember: hooks are initialized upon load. If you modify your settings.json to change the matcher from * to read, it won't take effect immediately. You have to exit out of Claude and restart it for the new hook configuration to initialize.
Bypassing Claude with system_message
When configuring a hook's command (for instance, a simple Node execution), you can pass a system_message parameter inside the JSON payload.
This parameter is fascinating. When you include a system_message, it prints your output directly to the terminal without going through Claude at all. It's raw, immediate feedback. (Note: as of right now, this direct terminal output doesn't work inside the VS Code integrated terminal version, but it works perfectly in a standard terminal.)
So, if we have our matcher set to read and we tell Claude, "Please read qr.js," the terminal will instantly spit out our custom system message—like "Hook fired: read"—proving our Node.js code ran successfully right before the read command executed.
Running External Scripts
Inline commands in settings.json are great for quick tests, but what if you want to do something more complex? You can easily point your hook to an external script.
Inside my .claude/hooks folder, I created a small JavaScript file. Instead of just printing a system message, this script does two things: it outputs which hook was fired, and it appends a log entry to a custom hooks.log file.
To invoke this, you just update your settings.json command to call node hooks/hook.js instead of running the inline Node command. After restarting Claude to reinitialize the settings, asking Claude to read a file will silently execute your external script. You can open up hooks.log and watch the entries populate in real-time.
If you run into a tiny syntax error while setting this up—which happens to the best of us—you can always just ask Claude to fix it. But by setting up the logging manually, you have concrete proof of exactly what is happening and when. You aren't just taking the AI's word for it; you have a transparent view of the mechanics.
If you need help with software development, or particularly if you're looking for Salesforce development expertise, I am a freelance developer and I'd love to help you out.