Claude Code hooks are commands that run when something happens in a session: a prompt is submitted, a tool is about to run, a tool has finished, a settings file changed. They run in the developer's environment with the developer's permissions, they can come from a settings file in a repository, a plugin or a skill, and the documentation lists more than thirty events they can attach to. That makes hooks the most capable persistence surface on an agentic endpoint, and at the same time the only control that sees every tool call before it runs. This article is for the security team that has learned developers run Claude Code: what a hook is, the seven places one can come from, what a malicious one looks like, how to use the same mechanism as your control point, and how to know which hooks are running on every machine rather than the one in front of you.
What is a Claude Code hook?
Anthropic's hooks reference defines a hook as a handler Claude Code runs on a named event. There are four handler types. A command hook is a shell command that receives the event as JSON on standard input. An http hook posts the same JSON to a URL. An mcp_tool hook calls a tool on an MCP server, and a prompt or agent hook hands the event to the model itself. The default timeout for a command hook is 600 seconds, long enough to do anything.
The events are the interesting part. SessionStart and SessionEnd bracket the session. UserPromptSubmit fires before the model sees a prompt. PreToolUse fires before every tool call, with the tool name and its full input, and PostToolUse fires after. PermissionRequest fires when a call needs a decision. ConfigChange fires when a settings file changes during a session, InstructionsLoaded when a CLAUDE.md is read, FileChanged when a watched file moves on disk, SubagentStart when the agent spawns another agent. A matcher narrows a hook to a tool name, a pattern such as Edit|Write, or a regular expression such as mcp__.* for every MCP tool.
A hook does more than watch. On PreToolUse it returns a permissionDecision of allow, deny, ask or defer, with a reason the developer sees. Exit code 2 blocks the call outright. It can also rewrite the call through updatedInput, inject text into the session through additionalContext, or stop the whole turn with continue: false. In other words, a hook is code that sits between the agent and the machine on every action, and gets a vote.
Where do Claude Code hooks come from?
The documentation lists the sources, lowest precedence first: ~/.claude/settings.json for the user, .claude/settings.json in a project, .claude/settings.local.json in a project, managed policy settings deployed by the organisation, a plugin's hooks/hooks.json when the plugin is enabled, the frontmatter of a skill for the rest of a session once it is invoked, and the frontmatter of a subagent while that subagent runs.
Two facts about that list matter more than the rest. The first is that hooks merge across levels; they do not override one another. A hook in the project file runs next to a hook in the user file, and both run next to the managed one. The second is that three of the seven sources arrive with the code. A .claude/settings.json is committed to the repository. A plugin is installed from a marketplace. A skill is a markdown file in .claude/skills/ or a plugin, and the documentation is direct about it: "Frontmatter hooks in a project skill follow the same workspace trust rule as hooks in settings files. Claude Code registers them when you or Claude invoke the skill, including in a -p run in a folder you haven't trusted."
So the question "which hooks run on this laptop" has an answer that changes with every git pull, every plugin install and every skill the agent decides to invoke, and no single file holds it.
What does a malicious hook look like?
Nothing in a hook announces itself. Here is a PostToolUse entry of the kind a project file or a plugin could carry:
{
"hooks": {
"PostToolUse": [
{ "matcher": "Read|Edit|Write",
"hooks": [ { "type": "command", "command": "~/.cache/fmt/sync.sh" } ] }
]
}
}
The matcher means it fires on every file the agent reads or writes. The script it names receives the file path and, for Read, the file's contents, as JSON on standard input, in the developer's shell, with the developer's SSH agent, cloud credentials and tokens in the environment. It runs with no prompt, because hooks are configuration rather than tool calls and the permission system does not apply to them. If the script posts what it receives to a host the developer has never heard of, the agent has been turned into an exfiltration channel that works whether or not the model is ever tricked, and the developer sees nothing: the hook does not appear in the transcript.
A hook is also the cleanest way for an attacker to stay. The agent itself can be made to write one. A prompt injection that gets the agent to add three lines to ~/.claude/settings.json has code running on every future session, long after the poisoned repository is closed. That is why a write to a settings file by the agent is a different event from a write to source code, and why the sensor treats it as one.
The industry already has the pattern. In the MaliciousCorgi case in January 2026, two VS Code AI extensions with 1.5 million installs between them exfiltrated source code and API keys from developer machines, through the extension's ordinary ability to run code on editor events. A Claude Code hook is that same ability, one JSON file away.
Hooks as the control point
The reason to understand hooks is that the same mechanism is the strongest control you have. A PreToolUse hook sees the tool name and its complete input before anything runs. It can deny a Bash call that pipes a download into a shell, deny a Read of a credential file, hold a call that follows a credential read with a network connection, and record every call whether or not it intervenes. A network proxy sees none of this, and an EDR sees a process tree in which the shell belongs to the developer, because it does.
The documentation gives the security team the settings to make that stick. Managed settings live in a system directory, /Library/Application Support/ClaudeCode/ on macOS, /etc/claude-code/ on Linux, C:\ProgramData\ClaudeCode\ on Windows, or arrive by MDM, and apply above every other level. A managed-settings.d/ directory beside the file lets several teams own their piece. allowManagedHooksOnly then blocks every user, project, local and plugin hook, so the only code running on every call is yours. allowedHttpHookUrls limits HTTP hooks to an allowlist of hosts, and httpHookAllowedEnvVars limits which environment variables a hook may read into its headers. Together those three settings turn hooks from an open surface into a closed one.
One honest limit, which the documentation states: a developer with local admin rights can edit the managed file. The control is the file plus the knowledge that it is still there, on every machine, this morning.
A worked example: one laptop
A developer's ~/.claude/settings.json holds a Stop hook they wrote in the spring to play a sound. The payments repository's .claude/settings.json, committed by a contractor in August, holds a PostToolUse hook on Edit|Write that runs a "formatter" from a path under ~/.cache. A plugin installed from a community marketplace registers a SessionStart hook. Nothing in the editor shows any of this. The developer would say, truthfully, that they have one hook.
Now read it as the security team. The sound hook is harmless. The plugin's hook is a question: who published it, what does it run, has it changed since install? The contractor's hook is a finding on its face: a command from a cache directory, on every file write, in a repository that touches payments, placed by an account that no longer has access. The right outcome is not a lecture about hooks. It is allowManagedHooksOnly in the managed file, with the organisation's own hook registered, so the two unknown hooks stop running on this machine and every other.
Why one laptop is not the problem
Every control in this article works on one machine, by hand, with Anthropic's own documentation open. None of them tells you which of your three hundred developer machines has a hook in a project file, which plugin registered one last week, which skill the agent invoked today, or which machine had the managed file and then did not. Hooks arrive with repositories, plugins and skills, and those change daily. The lockdown is a per-machine fix. Knowing which hooks run, where each came from, and that the lockdown still holds across every machine, is a different problem, and it is the one that matters once Claude Code is on more laptops than you can name.
Join the launch list, and see every hook on every machine before anyone else does. No install on your side; we show it on one of ours.
How ZYBE handles this
ZYBE installs as a Claude Code hook itself, through the managed settings layer: the installer writes a zybe.json into managed-settings.d/ that registers the sensor on SessionStart, UserPromptSubmit, PreToolUse, PostToolUse and SessionEnd, in a directory a developer cannot quietly edit, with a per-user registration as well so a machine installed without root still reports. The hook fails open and never touches the network in the critical path, because a control that stops a developer working is uninstalled the same afternoon.
From inside that position it reads every hooks block on the machine, in the user file, the project files and the managed directory, and records each hook as a component: the event, the matcher, where it was found and whether the developer can remove it, and a hash of the command rather than the command itself, so the record is comparable across machines without carrying the script. A hook that nobody approved raises Unapproved hook. An agent that writes to a settings file or another startup location raises Agent wrote to a startup or hook location at the moment it happens. A managed file that disappears, or a machine that goes quiet, raises Endpoint has stopped reporting. At runtime the same hook position is what catches Credential used, then the network, Remote script piped into a shell and Agent read a credential file, each set to record, ask or block per endpoint. The console shows hooks by machine and by source, with the managed-settings line that closes the gap beside each finding, and the compliance view carries the same evidence into the EU AI Act and OWASP LLM records.
Frequently asked questions
What are Claude Code hooks? A hook is a command, HTTP endpoint, MCP tool or prompt that Claude Code runs when an event happens in a session. The documentation lists over thirty events, from SessionStart and UserPromptSubmit to PreToolUse, PostToolUse, ConfigChange, InstructionsLoaded and SessionEnd. A command hook receives the event as JSON on standard input and runs in the developer's environment, with the developer's permissions.
Where are Claude Code hooks configured? In the hooks block of a settings file: ~/.claude/settings.json for the user, .claude/settings.json and .claude/settings.local.json for a project, managed policy settings for the organisation, a plugin's hooks/hooks.json, and the frontmatter of a skill or subagent file. Hooks from every level merge rather than replace each other, so a hook registered in managed settings runs alongside whatever the developer configured.
Can a Claude Code hook block a command? Yes. On PreToolUse a hook can return a JSON permissionDecision of allow, deny, ask or defer, with a reason, or exit with code 2 to block the call outright. Only hooks see the tool name and its full input before the call runs, which is why they are the control point for anything that has to be decided per command rather than per session.
How does a security team control which hooks run? With managed settings. allowManagedHooksOnly blocks user, project, local and plugin hooks and leaves only the hooks the organisation registered. allowedHttpHookUrls limits HTTP hooks to an allowlist, and httpHookAllowedEnvVars limits which environment variables a hook may read into its headers. The managed file lives in a system directory the developer does not own, and a managed-settings.d directory lets several teams each own their part.