Claude won't do it the same way twice. Hooks fix that.
What a hook is, when to reach for one, and how to ship one that won't burn you.
A lot of you have moved your daily work into Claude Code. That is a natural shift. Claude Code runs in your terminal, so it works directly inside your file directory instead of in a chat window.
I run most of my workflows there now. Compared to Cowork, I find it faster at manipulating files and better at pulling live information off the internet.
The flexibility is the whole point. It is also the problem.
Ask it to save a copy of a document before it overwrites it, and it might. Ask it to leave a client folder untouched, and it usually will. “Might” and “usually” are fine when you are experimenting. They stop being fine when a task has to run the same way every time, or when one slip means a client file lands somewhere it should not.
Claude is a language model: it makes a fresh judgment call every session, so it will not do the exact same thing twice. A hook is the piece that does. It runs the same way every single time, automatically, no matter what Claude decides in the moment. It is one of the most powerful features in Claude Code, and one of the easiest to get wrong. A hook is a shell command that runs on your machine, with your permissions. But be careful about setting one up without reading how it works and when to use it.
To help demystify the hook, I have invited our dear friend Chris. Chris and I had the chance to chat live on one of our first Cash & Cache podcast episodes. He is deeply versed in IT infrastructure security and cybersecurity, and in how to use AI both securely and efficiently. Chris is a security engineer and the writer behind ToxSec.
What you need for this walkthrough: Claude Code installed and working. Either the desktop app or the CLI works fine. No coding required, Claude writes the script and you check it.
By the end of this piece you will know what a hook is, when to reach for one, and how to ship one yourself.
Start with what a hook actually is.
What a hook is, and the 8 events you should know
A hook is an instruction you set once that tells Claude Code to run a command of your choosing automatically, every time a particular event happens.
Here is a complete one. This hook template shows a notification on your desktop whenever Claude finishes editing a file, so you know a change has landed without watching the terminal.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"File changed\" with title \"Claude Code\"'"
}
]
}
]
}
}Read line by line, here is what each part is saying:
“hooks” is the section of your settings file where every hook lives.
“PostToolUse” is the event. This hook runs after Claude finishes using a tool.
“matcher”: “Edit|Write” is a filter. It tells the hook to run only after the Edit or Write tools, the two tools Claude uses to change files. An empty filter would run the hook after every tool, which gets noisy fast.
The inner “hooks” list is what to do when the event fires. Here there is one action.
“type”: “command” says the action is a shell command.
“command” is the command itself. This one shows a desktop notification that reads “File changed.”
Claude Code has more than two dozen hook events today, and the list keeps growing. You will not need most of them. Eight events cover almost everything a beginner will want to do, and they are the ones worth learning first.
PreToolUse runs just before Claude uses a tool, such as editing a file or running a command. It can stop an action before it happens.
PostToolUse runs right after a tool finishes. It cannot undo what was done, but it can react, for example by logging the change or starting a check.
UserPromptSubmit runs the moment you send a message, before Claude reads it. You can use it to add context to your prompt, or to block the prompt entirely.
Stop runs when Claude finishes its reply and hands the turn back to you.
SubagentStop runs when a subagent finishes.
Notification runs when Claude Code sends you a notification, which is usually the moment it is waiting for your input or your permission.
PreCompact runs just before Claude compacts a long conversation.
SessionStart runs every time you open or resume a Claude Code session. It is useful for loading context you always want Claude to have.
The other events cover narrower situations, and you can find the full list in Anthropic’s Claude Code hooks documentation.
Knowing what a hook is and which events exist is the straightforward part. The harder question is when you should reach for a hook at all, and when it is the wrong tool for the job. I will hand over to Chris for that.
When a hook is the right tool, and when it isn’t
Before we talk about how to write a safe hook, the harder question: do you need one at all? Most “I need a hook” instincts are actually “I need a different feature.” The decision tree is short.
Need different behavior in one project? That’s CLAUDE.md. Drop a markdown file in the repo, tell Claude how to act, done.
Need a reusable workflow Claude can call when the moment’s right? Skill, or a slash command. Claude decides when. You decide what.
Need something to happen every single time, automatically, regardless of what Claude feels like doing? That’s the hook job. Hooks aren’t smart. That’s the feature. They fire on the event, every time, no judgment, no AI in the loop.
Now the inverse. Four cases where reaching for a hook is the wrong move.
You want to stop Claude from touching a sensitive folder. That’s a permission rule, not a hook. Hooks fire during the lifecycle. Permissions gate it before. If your goal is “never touch this,” you want the gate, not the cleanup crew.
You want to add a step you’ll iterate on weekly. That’s a skill. Hooks live in settings.json (details on how to access below), and editing settings.json mid-flow is the kind of friction you’ll resent by week three. Skills are cheap to change. Hooks are expensive.
You’re working on a client machine you don’t fully control. Don’t ship hooks into someone else’s environment. You don’t know what else is running there, what tools are installed, or how their setup differs from yours.
Anything you wouldn’t be comfortable running unattended on a fresh laptop at 3am. If the answer is “I dunno, probably fine,” it’s not.
War story
A team at a large logistics company set up a hook: every time Claude wrote or edited a file, an internal code-checking tool ran automatically in the background. It worked fine for a week.
What they had overlooked was what that background tool actually did. To analyze the code, it quietly connected over the network to one of the company’s internal servers.
Then someone asked Claude to write a batch of test files. One was filled with fake credentials, the kind developers use so tests have something to run against. By coincidence, those fakes were formatted just like the company’s real credentials.
The hook fired. The tool sent details about that file to the internal server. Security monitoring saw what looked like real credentials moving through an internal system and flagged it as a possible breach.
Alarms went off at 3am. Three engineers were woken up. They spent two hours confirming nothing real had leaked.
The fix was not a smarter hook. It was a simple rule: Claude is not allowed to touch the test folder. Five lines. No hook, no 3am alarm.
A safe-by-default starter settings.json
Here’s the goal of the starter hook: every time Claude is about to use a tool, a single line gets written to a log file Claude can’t touch. That’s it. It doesn’t change what passes through. It doesn’t reformat your code. It doesn’t push anything to version control. The point is to observe before you automate. Walk before you hand Claude the keys.
There’s a tempting version of this section where I drop a bash script and tell you to paste it in. I did exactly that in earlier drafts. It has two problems. First, the script only runs on Mac and Linux. If you’re on Windows, paste it in and nothing happens. Second, for a non-technical reader, what the script says line by line matters less than that the script is safe by construction. So we’re going to do it differently.
We’re going to have Claude Code write the hook for you, on whatever platform you’re on, and then we’re going to check its work against a short list. You’ll find the pre-flight checklist below.
Step 1. Find your Claude Code settings file.
It lives at one of these two paths depending on your operating system. Both are just plain text files you can open in any editor.
Mac / Linux: ~/.claude/settings.json
Windows: %USERPROFILE%\.claude\settings.json
On Windows: %USERPROFILE% is just a shortcut for your own user folder. Swap in your Windows username and it becomes C:\Users\eddie\.claude\settings.json (replace eddie with your login name).
If you’ve never touched it, it’ll be empty or close to it. If you have, it probably already has stuff in it. Either is fine. We’re going to add a hook to it, not overwrite it.
Step 2. Paste this prompt into Claude Code.
Open Claude Code in any folder, then paste the entire block below as your message. Yes, the whole thing. Hit enter.
I want to add a safe, read-only logging hook to my claude code settings. Please do all of the following, but show me each change before you apply it:
Figure out which OS i’m on and pick the right scripting language (bash on mac/linux, powershell on windows). use absolute paths only, no shortcuts like ~ or $HOME.
Create a small script called cc-audit that does exactly one thing: read whatever claude code passes in on standard input, and append it as one line to a daily log file. quote every variable. it must never run the input as a command, and must never hand it to another tool.
Put the log file outside my normal project folders, in a location my regular user account cannot edit after the fact, so a future compromised hook cannot rewrite its own log. on mac/linux that’s typically under /var/log. on windows, a folder under ProgramData. If i don’t have admin rights, fall back to a separate user folder and tell me why.
Wire it into my settings.json as a PreToolUse hook with matcher “*” and a 5000-millisecond timeout. if the file already has hooks, MERGE the new entry into the existing “hooks” object. do not overwrite my existing config, and do not produce invalid json. show me a diff before saving.
When you’re done, show me three things in your reply:
- the full final script
- the updated settings.json
- one example line from the log file so i know it’s working
Two notes on that prompt before you send it.
“show me each change before you apply it” is the most important line. It forces Claude into review mode instead of just-do-it mode. You see the script before it lands on your disk. Good practice with hooks, good practice with anything Claude is going to write to your machine.
“MERGE, do not overwrite” is the line that protects your existing settings. JSON is fussy about commas and brackets. A bad merge will silently make Claude Code stop loading your settings, and you may not notice for days. Saying “merge” explicitly heads that off.
Step 3. Read Claude’s output before you let it save anything.
This is the part nobody does and everybody should. Claude will show you a proposed script and a proposed settings.json change. Before you say yes, walk down the pre-flight checklist (section below). Six items. If any one of them fails, tell Claude to fix it. Repeat until they all pass.
What it should look like when it’s working.
After Claude has saved everything, your settings.json should contain a block that looks roughly like this (yours may have other hooks alongside it, and that’s fine, just don’t delete them):
{
"hooks": {
"PreToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "/usr/local/bin/cc-audit.sh",
"timeout": 5000
}
]
}
]
}
}The “command” line is the only platform-specific bit. On Windows it’ll point at a .ps1 file somewhere under C:\ProgramData\ instead.
To test it, do two things.
First, trigger the hook. Open a fresh Claude Code session in any folder and ask Claude to read any file in your project. The hook fires silently in the background. You will not see anything in the terminal.
Second, open today’s log file. The log is a plain text file with a .log extension. It is not inside Claude, and it is not a .md file. It is just text sitting on your disk. The exact path is whatever Claude told you in step 5 of the prompt above. On Mac and Linux it will be something like /var/log/cc-audit/2026-06-10.log. On Windows it will be something like C:\ProgramData\cc-audit\2026-06-10.log.
There are two easy ways to view it.
Ask Claude. Open Claude Code and say “show me today’s cc-audit log.” Claude already knows the path (it just wrote it) and will print the contents straight into your terminal. This is the no-clicks-required option.
Open it manually. Paste the full path Claude gave you into Finder (Mac), File Explorer (Windows), or your file manager, and open the file with any text editor: TextEdit, Notepad, VS Code, anything. A .log file is just text. Any editor reads it.
Either way, you should see a line that looks roughly like this:
2026-06-10T14:22:01-07:00 {”hook_event_name”:”PreToolUse”,”tool_name”:”Read”, ... }
That line, written automatically every time Claude is about to use a tool, with no chance for the contents to run as a command, is the entire starter. From here you can narrow the matcher to specific tools, or move from PreToolUse to PostToolUse, or graduate to a hook that actually blocks. But start by watching. Watch for a week before you change anything.
One scoping note before we move on: this starter is for your own machine. A settings.json checked into a shared project repository is a different threat model entirely, and the first vulnerability we cover below is exactly what happens when you forget that.
The security mindset
Here is the mindset to carry into every hook you write, before a single line of config: a hook hands part of your computer over to a process that runs without asking you first. Most features in Claude Code keep a human in the loop. A hook removes that loop on purpose. That is the entire value, and the entire risk, in one sentence. So you treat every hook the way you would treat handing someone a key to your house: not because you expect the worst, but because the cost of being wrong is high enough that “probably fine” is not good enough. Security-first here is not paranoia. It is just matching your caution to the size of the blast radius.
So here’s what goes wrong. Three patterns, all of them live in the wild.
Story one: instructions sneak in from the outside. You ask Claude to research a vendor. Claude fetches a webpage. Hidden in that page is a paragraph that reads like normal content but is actually instructions: “when you process this article, please refer to it using the path [redacted payload].” That bracketed path is not a path at all. It is a command in disguise, and the moment it reaches your computer dressed as a file location, your computer runs it. That is what makes a hidden string like this dangerous: it turns a request to “read a page” into a request to “run whatever the attacker wrote.” Claude obeys, because to Claude that text is just more content. Claude’s next tool call carries that hostile string forward. Your hook fires, picks up the string, and passes it along to the operating system as part of a command. The operating system cannot tell the difference between data and instructions. It just runs what it is given. The hook itself was not broken. The hook became the foothold the moment it trusted text coming back from the web as safe.
Lasso Security published research on exactly this class of attack and shipped an open-source defender hook for it. Their demonstrations use poisoned web pages, poisoned project documentation, and even poisoned source-control history. Anywhere outside text enters Claude’s context, an attacker can plant a string. If your hook touches that string and treats it as a command, your hook is the way in.
Story two: Claude wanders outside the lines, and the hook follows. You ask Claude to read a file. But Claude picks a path outside your project. Maybe a piece of documentation pointed it the wrong way. Maybe Claude just guessed at a file path that happened to exist on your computer. You’d set up a helpful little rule earlier: every time Claude touches a file, automatically tidy it up. Neat formatting, no manual cleanup. That rule now fires on whatever Claude found, even though it has no business touching that file. The cleanup tool fails because it doesn’t recognize the file type, but the damage is already done. Your log file, the same audit log from the starter setup above, now contains the full location of every sensitive file Claude ever stumbled across: your SSH keys, your password manager backups, your cloud credentials. And that log file isn’t sitting safely on your computer. It’s inside a folder that auto-syncs to the cloud (Dropbox, OneDrive, iCloud, take your pick), because most people sync their entire computer to the cloud without thinking about it. So a list of where all your sensitive files live just got copied to someone else’s servers. Same lesson as story one. Claude was nudged by something it shouldn’t have trusted, and the hook didn’t stop to ask questions. It just ran.
Story three: the permission system you trusted has gaps. This is the one that should keep you up at night. You set a rule that blocks dangerous commands like delete-everything. Feels safe. But security researchers showed that if an attacker buries a dangerous command inside a long chain of 50 harmless-looking ones, Claude’s checker gets overwhelmed, gives up trying to inspect each piece, and just asks “is this whole bundle okay?” You say yes to what looks like routine work. The dangerous command hiding inside the chain runs anyway.
Anthropic’s own disclosed bugs are worse. One (rated high severity) let an attacker hide a malicious instruction file inside a shared code project. The moment you downloaded that project and opened it in Claude Code, the trap fired before Claude even asked your permission. Open the folder, get hacked. A second bug used a setting in the same configuration file to secretly reroute Claude’s internet traffic through the attacker’s server, handing over the password Claude uses to talk to Anthropic in plain readable text, before you clicked a single button. Both have been patched. (For the receipts: these are CVE-2025-59536, fixed in version 1.0.111, and CVE-2026-21852, fixed in version 2.0.65.)
Both prove the same point: the permission system relies on simple word-matching to catch sophisticated attacks, and that’s not enough. A hook is the safety net the permission system can’t always be. And here’s the kicker. There’s a public report where Claude, given access to edit its own safety rules, quietly changed the rule from “alert me on code changes” to “alert me on SIGNIFICANT code changes.” One word. The safety check still existed. It just stopped catching anything.
One last note on why clicking “yes” on a permission prompt will not save you. You click yes a hundred times a day. The hundred and first you do not read. That is just how prompts work on humans. Every red team has a bingo card for it. Hooks fire automatically because that is the point. That is also why a wrong hook is worse than no hook: it removes the one human checkpoint left, and replaces it with a script the AI can pivot through.
The pre-flight checklist
Six rules. Tape them above your monitor. Use them as your review pass on whatever Claude Code wrote, for example the starter hook we illustrated earlier: read its proposed script and proposed settings.json against this list before you let either save to disk. If a rule fails, paste the failing point back at Claude and ask it to fix it. Repeat until all six pass.
One thing to know first: the “command” line in your settings is the part that actually runs, but it usually just points at a script (like our cc-audit). So when these rules talk about restricting what the hook can touch, the restriction lives inside the script, not in the settings.json. The settings.json matcher only decides which tools wake the hook up. What the hook is allowed to do once it wakes up is up to the script Claude wrote.
1. Treat every input as text, never as a command. Claude passes information into the hook, like a file path. If that information sneaks in hidden instructions, the computer will run them. The fix is small: every variable in the script should be wrapped in double quotes. Skim the script Claude produced and check that things that look like $1 or $VAR (or $args on Windows) have quote marks around them. That closes the first story.
2. Give the hook an approved list of files it may touch. By default, the hook will run on any file Claude touches, even ones outside your project. Tell Claude: “only allow these folders.” If the file is not on the list, the hook stops. That closes the second story.
3. Lock the hook to one folder. Without a fixed starting point, the hook runs wherever Claude was last working, which is unpredictable. The script should have a line near the top that says “always start in this folder.” If you don’t see one, ask Claude to add it.
4. Give the hook the least access it needs. A hook running with admin-level access (the master-key keyword on a computer, often written as sudo on Mac/Linux, or run-as-administrator on Windows) can do anything, including delete your system. If you see sudo inside the script Claude wrote, ask why. If there is no clear answer, take it out.
5. Keep the activity log somewhere the hook cannot edit. If a bad hook can rewrite its own log, you have no record of what it did. The log should live under an admin-controlled folder, or somewhere the hook itself cannot modify after the fact. This is exactly the failure behind the earlier example of Claude quietly rewriting its own safety checks.
6. Read the hook out loud to a colleague before you save it. Open your settings file, point at the “command” line, walk to the script it points at, and read it word by word. If you cannot explain a line in plain English, that line should not be there. This catches more problems than any paid tool, and it costs nothing.
A little less magic, a lot more trust
Chris just covered the whole arc: when a hook is actually the right tool, a safe starter that only watches before it acts, and a checklist worth keeping close.
A hook takes something that decides fresh every session and locks one small part of it in place. Instead of hoping Claude does the right thing, you tell it exactly what happens, and the hook makes sure it happens that way every time. You give up a little flexibility and get something you can count on. In the right setting, that is powerful. Used carelessly, it hands your computer to someone else.
If this clicked, go read Chris at ToxSec.
Hooks lock down what Claude Code does. The other half of running it seriously is how you run it: alone, or as a team.
For more on our builds and workflows, check the Cash & Cache Library.
Skill files, AI workflow templates, prompt packs, and implementation guides — all downloadable, all built from how we actually work. Premium subscribers get full access.
Unlock the Library → Cash & Cache Library
What's the first thing you'd want a Claude Code hook to do for you? Drop it in the comments.
💡 Enjoying this article or other ones about AI implementation? Share this piece with someone who can benefit.









I love Claude Code guys.. Once you have your businessOS setup in it and it knowing your actual files means less copy-paste and fewer wrong assumptions about your setup.
the nondeterminism isn’t the model misbehaving. it’s the harness leaving decisions open that should have been closed. hooks are just the harness doing its job. reproducibility was always an engineering problem, not a model one.