anotifier / guides / claude-code-hooks-security
[ GUIDE · CLAUDE CODE HOOKS SECURITY ]
Claude Code logo

Claude Code hooks security: what hooks can run and how to audit them

A Claude Code hook is a shell command that runs with your full user permissions. Anyone who can put one in a settings file, a plugin or a repository you trust can run code as you. Hooks come from your user, project and local settings files, from plugins, from skill and subagent frontmatter, and from managed policy. Type /hooks to see every active hook and where it came from. Set disableAllHooks to switch them all off, and an admin can set allowManagedHooksOnly to block everything except managed hooks. Codex CLI, Gemini CLI and Cursor work the same way, each with its own review step. The last section lists exactly what anotifier's own hooks do.

Rather skip the manual setup? One command wires every agent you have installed to desktop toasts, phone push, and Slack, Discord or Telegram:

$ npx anotifier@latest setup

Updated 2026-10-12 · also see the Claude Code notifier and the full docs

What a hook is allowed to run

A Claude Code command hook runs a shell command and passes it the event as JSON on stdin. The Claude Code docs put it plainly: command hooks execute shell commands with your full user permissions, and can modify, delete or access any file your user account can. A hook process also inherits Claude Code's environment, apart from the OTEL_* exporter variables and, when CLAUDE_CODE_SUBPROCESS_ENV_SCRUB is set to 1, the variables Claude Code strips. So an API key exported in your shell is visible to every hook.

  • What it receives. Every event carries session_id, transcript_path and cwd. Tool events add the tool name and its input, so a PreToolUse hook sees the command or file path Claude is about to use.
  • What it can change. A hook can do more than watch. A PreToolUse hook can deny a tool call or rewrite its input, so a hook can change what Claude does as well as read it.
  • Other handler types. Besides command, a hook can be http (the event JSON is sent as a POST request to a URL), mcp_tool, prompt or agent.

Where Claude Code hooks come from

LocationApplies toShared with others
~/.claude/settings.jsonAll your projectsNo
.claude/settings.jsonOne projectYes, it can be committed to the repo
.claude/settings.local.jsonOne projectNo, gitignored when Claude Code saves a setting to it
Managed policy settingsThe whole organizationYes, set by an admin
A plugin's hooks/hooks.jsonWhile the plugin is enabledYes, bundled with the plugin
Skill frontmatterThe rest of the session once the skill is invokedYes, defined in the skill file
Subagent frontmatterWhile that subagent is runningYes, defined in the subagent file

Entries add up. User, project and local hooks are merged rather than replacing each other, and none of them can remove a managed hook. Hooks from settings files, managed policy and plugins also run inside subagents.

The real risks

  • A repository you cloned. A project's .claude/settings.json can hold hooks and is meant to be committed. In an interactive session, Claude Code holds back hooks from every settings file until you accept the workspace trust dialog for the folder, or for a parent folder whose trust extends to it. Once you accept, the repository's hooks run. In a claude -p or SDK session no dialog ever appears and the folder counts as trusted, so the repository's hooks run straight away.
  • Hooks that change under you. Claude Code picks up edits to hooks in settings files automatically. After a git pull in a repository you already trust, read the diff of .claude/ as well as the code.
  • Plugins. An enabled plugin's hooks run with the same permissions as hooks you wrote yourself. Read a plugin's hooks/hooks.json before you enable it.
  • Secrets. A hook sees your whole environment, the session's transcript_path and the inputs of tool calls. A command can hand any of that to another program. Claude Code's own advice for hook authors includes skipping sensitive files such as .env, .git/ and keys.
  • Network exfiltration. A command hook is an ordinary command, so it can make network requests. An http hook posts the event JSON to the URL it names. Two settings narrow the second kind: allowedHttpHookUrls limits which URLs an HTTP hook may call, and httpHookAllowedEnvVars limits which environment variables go into hook headers.

How to audit the hooks you have

  1. List them. Type /hooks in Claude Code. It is a read-only browser. It labels each hook with where it comes from (user settings, project settings, local settings, a plugin or the current session) and shows the full text of what it runs.
  2. Read the files. Open ~/.claude/settings.json, then .claude/settings.json and .claude/settings.local.json in each project, then the hooks/hooks.json of each enabled plugin. To remove a hook, delete its entry. Claude Code has no way to disable one hook and keep it in the config.
  3. Switch them all off while you look. Set disableAllHooks to true in a settings file. It also turns off a custom status line and a custom file suggestion command.
{
  "disableAllHooks": true
}

Claude Code reads the value left after settings precedence, so a project's .claude/settings.json that sets "disableAllHooks": false overrides a true in your user settings. To turn hooks off for one run whatever the project says, pass the setting on the command line, which takes precedence over project and local settings:

claude --settings '{"disableAllHooks": true}'

How to lock hooks down

For code you didn't write, the permissions docs suggest three options before you run claude -p there. Each stops the project's own hooks from running:

# read only your user settings, not the project's
claude -p --setting-sources user "your prompt"
# read no hooks, skills, commands, subagents, plugins or .mcp.json servers from the project
claude -p --bare "your prompt"
# turn hooks off for this run
claude -p --settings '{"disableAllHooks": true}' "your prompt"

Setting disableAllHooks in your user settings alone is not enough here, because the project's settings take precedence over yours.

For a team, an admin sets allowManagedHooksOnly in managed settings:

{
  "allowManagedHooksOnly": true
}
  • Still runs: hooks from managed settings, hooks the Agent SDK registers in process, and hooks from plugins your managed settings force-enable through enabledPlugins.
  • Blocked: user, project and local hooks, hooks from other installed plugins, and hooks declared in agent frontmatter.
  • Also narrowed: statusLine, fileSuggestion and subagentStatusLine are read from managed settings only.
  • Cannot be undone from below: only disableAllHooks set at the managed level can disable managed hooks.

The setting's reference entry lists the rest, including how command-sourced plugins are treated.

Claude Agent SDK hooks

The Agent SDK has hooks of its own: callback functions you register in your code with options.hooks, which run in your process. When the SDK collects the hooks for an event, it also includes shell command hooks from settings files, if the matching settingSources (TypeScript) or setting_sources (Python) entry is enabled. The SDK hooks docs say it is enabled for default query() options. So an SDK agent started in a folder can run that folder's project hooks.

When the folder isn't yours, set settingSources without project settings. Under allowManagedHooksOnly, hooks the SDK registers in process still run, and user, project and local settings hooks do not.

Codex CLI, Gemini CLI and Cursor

  • Codex CLI. Hooks live in ~/.codex/hooks.json, ~/.codex/config.toml, <repo>/.codex/hooks.json and <repo>/.codex/config.toml, in plugins, and in managed requirements.toml. Before a non-managed hook runs, Codex requires you to review and trust the exact hook definition. It records trust against the hook's hash, so new or changed hooks are skipped until you trust them. Run /hooks to inspect sources, trust hooks or disable individual non-managed hooks. Project hooks load only when the project's .codex/ layer is trusted. To turn hooks off, set hooks = false under [features] in config.toml. Admins can set allow_managed_hooks_only = true to ignore user, project, session and plugin hooks. The docs also say to treat tool hooks as a guardrail, not a complete enforcement boundary.
  • Gemini CLI. Hooks live in settings.json: .gemini/settings.json in the project, ~/.gemini/settings.json, /etc/gemini-cli/settings.json, and installed extensions. The docs say hooks execute arbitrary code with your user privileges and that project-level hooks are particularly risky in untrusted projects. Gemini CLI fingerprints project hooks. If a hook's name or command changes, for example after a git pull, it is treated as a new, untrusted hook and you are warned before it runs. /hooks panel lists them, /hooks disable-all turns them all off and /hooks disable <name> turns one off. Hooks run with a sanitized environment.
  • Cursor. Hooks live in .cursor/hooks.json in a project and ~/.cursor/hooks.json for you. Enterprise installs add a system-wide hooks.json (/Library/Application Support/Cursor/ on macOS, /etc/cursor/ on Linux and WSL, C:\ProgramData\Cursor\ on Windows), and Enterprise teams can sync hooks from the web dashboard. Project hooks run in any trusted workspace. The Hooks tab in Customize and the Hooks output channel show what is configured and what ran. Cursor reloads hooks.json when you save it, so removing an entry is how you stop a hook.

What anotifier's own hooks do

anotifier is a set of hooks, so it should pass the same audit. This section follows the source of anotifier 1.7.1: src/notify.mjs and setup/patch-config.mjs. npx anotifier setup edits your user-level files only, never a project's settings:

AgentFile setup editsEvents it registers
Claude Code~/.claude/settings.jsonNotification, Stop, and the statusLine command
Codex CLI~/.codex/hooks.json and ~/.codex/config.tomlStop, SessionStart, PermissionRequest
Cursor~/.cursor/hooks.jsonstop
Gemini CLI~/.gemini/settings.jsonAfterAgent, Notification
Antigravity CLI~/.gemini/config/hooks.jsonStop

Every entry runs node "<path>/src/notify.mjs" --source <agent>, a local script in the anotifier package. For Claude Code, setup also points statusLine at src/statusline.mjs, which drives the usage and context warnings. If you already had a statusline, anotifier wraps it and still prints your line. disableAllHooks switches this statusline off too. Installed as a Claude Code plugin instead, the same Notification and Stop hooks come from the plugin's hooks/hooks.json.

For Codex, setup also sets hooks = true in config.toml and writes the trusted_hash of each of its own hooks, so Codex does not ask you to review them. If you edit one by hand, Codex skips it until you trust it again in /hooks.

When a hook fires, notify.mjs does this:

  • Reads the event JSON from stdin and your settings from ~/.anotifier/config.json.
  • For Claude Code, it can read the session transcript so a toast or webhook shows Claude's last message. That is the richContent setting: on for toast and webhook, off for ntfy. Other agents get generic text.
  • Shows a desktop toast by running a local program: PowerShell 7 with BurntToast on Windows, osascript on macOS, notify-send on Linux, and PowerShell through Windows interop on WSL.
  • Writes small lock files and an errors.log under ~/.anotifier/, and always exits 0 so it can't fail your agent's turn.

The only network calls come from these channels. Each one is off unless you turned it on, except the version check:

DestinationWhenWhat is sentTurn it off
Your ntfy server (default https://ntfy.sh)ntfy enabled with a topicThe notification title and text. Claude's last message only with ntfy.richContentntfy.enabled: false
Your webhook URLWebhook enabled with a URLJSON with title, message, source, project, event and timestamp in the generic format. For Claude Code the message is Claude's last message unless webhook.richContent is falsewebhook.enabled: false
Your Sentry DSNsentry.enabled with a sentry.dsnHook and channel errorssentry.enabled: false
posthog.devino.caYou opted in to usage stats. At most once a day from hooksCounts and which features are on. Never message text, project names, paths, topics or URLsanotifier telemetry off, or DO_NOT_TRACK=1
registry.npmjs.orgAt most once every 24 hoursA plain request for the latest anotifier versionupdateCheck.enabled: false stops the hook's check

Setup itself downloads the notification icon from claude.ai once and, on Windows, installs the BurntToast PowerShell module for your user. Apart from the calls above, the hook path makes no network calls of its own. The README lists every usage-stats field.

To inspect and remove it:

npx anotifier@latest status     # which agents are wired, and with which events
npx anotifier@latest telemetry  # usage stats on or off, and what is waiting to be sent
npx anotifier@latest uninstall  # remove anotifier's hooks from every agent
  • uninstall asks first, backs up each file to ~/.anotifier/backups/, and removes only anotifier's entries, its Codex trust hashes and its statusline wrapper, which restores your own statusline. It keeps ~/.anotifier/, so delete that folder by hand if you want it gone. On Windows it also removes the agentfocus:// click-to-focus protocol that the first toast registers.
  • A plugin install is separate: remove it from Claude Code's /plugin menu. uninstall doesn't touch it.
  • /hooks in Claude Code and Codex, /hooks panel in Gemini CLI, and the Hooks tab in Cursor show the entries too.

Official docs this page follows

Checked on 2026-10-11. Docs change, so confirm a setting there before you rely on it.

Claude Code hooks security: common questions

Can a Claude Code hook run any command?

Yes. A command hook runs a shell command with your full user permissions, so it can read, change or delete anything your user account can, and it can make network requests. Claude Code's docs say to review and test every hook command before you add it.

Does Claude Code run hooks from a repository I just cloned?

Only after you accept the workspace trust dialog in an interactive session. Until then Claude Code holds back hooks from every settings file. In a claude -p or SDK session there is no dialog and the folder counts as trusted, so the repository's hooks run right away. Use --setting-sources user, --bare or --settings with disableAllHooks set to true for code you didn't write.

How do I see which hooks are active in Claude Code?

Type /hooks. It is a read-only browser that labels each hook with its source (user, project or local settings, a plugin, or the session) and shows the full text of what it runs.

How do I turn off all Claude Code hooks?

Set disableAllHooks to true in a settings file, or pass --settings with {"disableAllHooks": true} for one run. A project's settings take precedence over your user settings, so the flag is the reliable way for a single run. Only the managed settings level can disable managed hooks.

What does allowManagedHooksOnly do?

It is a managed-settings key. When true, only managed hooks run, plus hooks the Agent SDK registers in process and hooks from plugins that managed settings force-enable. User, project and local hooks and other plugins' hooks are blocked.

Do Codex CLI, Gemini CLI and Cursor review hooks before running them?

Codex skips new or changed non-managed hooks until you trust them, and loads project hooks only for a trusted project. Gemini CLI fingerprints project hooks and warns before a changed one runs. Cursor runs project hooks in a trusted workspace.

Is anotifier's hook safe to run?

It is a local Node script you can read. It runs with your permissions like any hook, sends notifications only to the channels you turned on (ntfy, a webhook, and Sentry if you set a DSN), and makes a version check against the npm registry at most once a day. Usage stats are off unless you opt in. Run npx anotifier status to see what is wired and npx anotifier uninstall to remove it.

Try it

One command wires every agent you have installed: Claude Code, Codex CLI, Cursor, and Gemini CLI.

npx anotifier@latest setup

Other guides