Blog

Claude Code Hooks Reference: Setup and 7 Ready-to-Use Examples

Markdown

Claude Code hooks docs in practice: four handler types, lifecycle events, settings.json, seven examples, testing, and common pitfalls.

Read full article

Hooks in Claude Code are shell commands, HTTP calls, or short LLM checks that run automatically before or after a Claude action. They add checks to specific tool events, format code after an edit, and inject fresh context at session start. A matching PreToolUse hook can reject that call; these regex examples are convenience guards, not a security boundary across every file or Git access path. Next is a breakdown of 4 handler types, 31 lifecycle events, a test for “when a rule belongs in a hook,” 7 ready-to-copy hooks for .claude/settings.json, and 3 pitfalls I’ve run into myself.

Ailoot documentation: API, keys, and setup.

Claude Code hooks explained: what runs before or after an action?

Key takeaway

A hook is a shell command, HTTP call, short LLM prompt, or subagent that Claude Code runs in response to a specific lifecycle event. Unlike instructions in CLAUDE.md, which the model may ignore or forget, a hook runs the same way every time: it is code, not a suggestion.

For half a year, I kept the rule “never edit .env directly” in CLAUDE.md. Claude broke it once a month. Not maliciously; it simply forgot by the end of a long session. Once, an extra token ended up in .env; another time, a variable with a duplicate name overwrote the one in use.

I moved that rule into a hook, making it impossible to forget. When a matching direct edit tool supplies a .env path, the configured hook rejects that call. Bash redirection, scripts, MCP tools, and other access paths require separate controls; this example does not make the file immutable.

Hooks are shell commands, HTTP endpoints, or LLM prompts that you define and that run automatically at specific points in Claude Code’s lifecycle.

— Anthropic, https://code.claude.com/docs/en/hooks

In other words, a hook is your command that Claude Code runs on a specific event. Every file edit, Bash launch, session start, or subagent completion is an event you can attach a hook to.

Why this matters for a vibe coder:

  • Protect files containing secrets from accidental edits.
  • Automatically run prettier/eslint after every code generation.
  • Block git push --force to main without explicit permission.
  • Inject fresh context at the start of each session.
  • Log Claude’s actions for auditing.

CLAUDE.md handles context. Hooks handle actions. These are two different things, and confusing them is the main beginner mistake.

How are hooks different from CLAUDE.md and skills?

Key takeaway

CLAUDE.md is text the model reads at the start of a session and tries to follow. A hook is a shell command the Claude Code process runs in response to an event, independent of the model. A skill is a named package of instructions that Claude can invoke with the Skill tool, like a function. All three address different kinds of tasks.

Anthropic’s documentation explicitly explains the difference between CLAUDE.md and hooks:

CLAUDE.md content is delivered as a user message after the system prompt, not as part of the system prompt. Claude reads it and tries to follow it, but there is no guarantee of strict compliance.

— Anthropic, memory docs, https://code.claude.com/docs/en/memory

Put another way: CLAUDE.md content reaches Claude as the first message from the user, not as a system instruction. The model will read it and try to follow it. But there are no guarantees, especially toward the end of a long session, when the context grows and older instructions are pushed out.

A hook works differently. It lives in settings.json, does not depend on the model, and is run by the Claude Code process on a specific event: PreToolUse, PostToolUse, SessionStart, and others. The command runs before Claude receives the result and can physically block an action (exit 2) or modify a file after editing it (exit 0).

Comparison:

FeatureCLAUDE.mdHookSkill
Where it livesCLAUDE.md in the project root.claude/settings.json.claude/skills/ folder
Who runs itThe model reads it as part of a user messageThe Claude Code process on an eventThe model through the Skill tool
Execution guaranteeNo (best-effort)A configured matching command hook executes code; failures and exit behavior depend on the eventThrough a model call (with a grant, but not at the shell level)
What it’s forProject context, tone, style, routingRestrictions, formatting, checks on specific filesComplex multistep procedures with parameters
When it runsEvery model message in a sessionOn a lifecycle eventWhen invoked by the model
Can it block an action?NoYes (exit 2)No

The full CLAUDE.md template and 6 rules for a living file are in the guide How to configure CLAUDE.md in 2026: a ready-to-use template and 6 rules. Learn about skills as named capabilities in Claude Code Skills: How to build your own library.

A hook is the lowest level: no model, no interpretation, just shell.

Hooks are one of the context engineering tools, alongside Plan Mode and /compact.

4 hook types: Command, HTTP, Prompt, Agent—which should you choose?

Key takeaway

Claude Code has 4 handler types. Command is a shell script, the most common type. HTTP sends a POST request to your URL, for teams and integrations. Prompt makes a short Haiku call for a semantic check. Agent creates a subagent with tool access for deep validation. Beginners need Command and Prompt; they cover 90% of tasks.

TypeWhat it doesWhen to choose itComplexity
CommandRuns a shell command, receives JSON on stdin, and responds with an exit code and stdout90% of cases: checking paths, formatting, grep on content, lsof, git operationslow
HTTPSends a POST request to your URL with a JSON body and receives JSON in responseWhen a rule belongs to a team/CI: centralized validation, a shared status dashboard, remote auditingmedium (requires a server)
PromptSends a short prompt to Haiku (by default) and gets yes/no for a semantic assessmentWhen grep isn’t enough: “assess whether this text sounds corporate”low
AgentCreates a subagent with Read/Grep/Glob access for multistep checksHeavy validation: “find all changed files and check that corresponding tests exist”high

The most common mistake is trying to do with Command what calls for Prompt. For example, “don’t use em dashes in .md files” is a grep task, so Command is ideal. But “don’t write in a corporate tone” is semantic, so it needs Prompt.

If you’re just starting out, use Command. The 7 ready-to-use hooks below are all Command. HTTP/Prompt/Agent come next, when you’re ready.

Where is settings.json, and what are the 4 config levels?

Key takeaway

Claude Code looks for configuration in 4 places: enterprise (deployed through MDM), user (~/.claude/settings.json), project (.claude/settings.json in the repository root), and local (.claude/settings.local.json, not in git). All 4 files are merged at session start, with the lower level taking precedence.

LevelPathWhen it appliesIn git?
Enterprise/Library/Application Support/ClaudeCode/settings.json (Mac), /etc/claude-code/settings.json (Linux)Across the team through MDMno
User~/.claude/settings.jsonAll projects for one userno
Project.claude/settings.json in the repo rootThis project, for the whole teamyes
Local.claude/settings.local.jsonOnly this project, only this userno (in .gitignore)

I keep the 7 hooks described below in the project .claude/settings.json, so they’re included automatically when the repository is cloned and Claude follows the same rules in a new window. Personal experimental hooks go in .claude/settings.local.json so they don’t get in the team’s way.

All 4 files are merged at session start. If rules conflict, the lower level takes precedence (local over project, project over user).

The quickest way to create your first hook is the interactive /hooks menu right in Claude Code. Choose an event from the list, select a matcher (for example, Bash or Edit), and paste in a command. Claude Code writes valid JSON to settings.json for you.

Claude Code hooks list: which lifecycle events do beginners need?

Key takeaway

Claude Code currently has 31 lifecycle events, divided into 3 cadences: once per session (SessionStart/SessionEnd), once per turn (UserPromptSubmit/Stop), and on every tool call (PreToolUse/PostToolUse). Beginners need 4 of the 31 events: PreToolUse for restrictions, PostToolUse for automatic edits, SessionStart for context, and Stop for final checks.

Events occur at three frequencies: once per session (SessionStart, SessionEnd), once per turn (UserPromptSubmit, Stop, StopFailure), and on every tool call inside the agentic loop (PreToolUse, PostToolUse).

— Anthropic, hooks reference, https://code.claude.com/docs/en/hooks

It’s important to understand the three cadences so you don’t get confused:

  1. Once per session - SessionStart, SessionEnd, Setup. These run once when Claude Code opens and closes. Use them to initialize context and clean up temporary files.
  2. Once per turn - UserPromptSubmit, UserPromptExpansion, Stop, StopFailure. These fire on each of your messages and each time the model stops. Use them to log the conversation and perform final result checks.
  3. On every tool call - PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied, PostToolBatch, SubagentStart, SubagentStop. These fire on every file edit, Bash launch, read, or search. This is the hottest layer, where restrictions, formatting, and validations live.

The full list of 31 events includes asynchronous/reactive events: TeammateIdle, PreCompact, PostCompact, Elicitation, ElicitationResult, InstructionsLoaded, ConfigChange, CwdChanged, FileChanged, WorktreeCreate, WorktreeRemove, Notification. These are for advanced scenarios; skip them for now.

Five-question test: when does a rule belong in a hook, and when does it stay in CLAUDE.md?

Key takeaway

If a rule is about an action (not context), concerns safety/money/data, can be checked with grep/regex, is hard to undo if broken, and Claude has already broken it at least once, it belongs in a hook. If fewer than three of these five answers are “yes,” it stays in CLAUDE.md. I applied the test to my 80 lines, and 5 rules moved to hooks.

For me, this works like a law, not a suggestion. I boiled the choice between CLAUDE.md and a hook down to a simple five-question test. If at least three answers are “yes,” the rule goes in a hook. Fewer than three, and it stays in the text.

The “when does a rule belong in a hook?” test:

  1. Is this rule about an action, rather than context? (“don’t run rm -rf” is an action; “I have a TypeScript project” is context.)
  2. Is this rule about safety, money, or data? (“don’t touch .env,” “don’t publish keys.”)
  3. Can you check it with grep, regex, or a shell command’s exit code? (“after editing .md, check that there’s no em dash.”)
  4. If the rule is broken, would the consequences be painful to undo? (“force push to main” is hard to undo; “forgot a period at the end” is easy.)
  5. Have you already seen Claude forget this rule at least once?

Example: “Don’t use em dashes; use hyphens only.” Action? Yes. Safety? No. Checkable with grep? Yes. Hard to undo? No. Seen it broken? Yes. 3 out of 5: put it in a hook.

Counterexample: “I have vibe coders, not developers; write without slang.” Action? More like context. Safety? No. Checkable with a regex? Too difficult. Hard to undo? No. Seen it broken? Yes. 1 out of 5: keep it in CLAUDE.md.

Five of my rules passed the test. They moved to hooks. The remaining 75% stayed in the text file.

7 ready-to-copy hooks

Key takeaway

Below are 7 ready-to-use hooks for .claude/settings.json: block .env edits, run prettier automatically, prevent force-pushing to main, check for em dashes, free up a port before npm run dev, auto-commit after successful tests, and inject fresh context at the start of a session. Copy each into the file, restart Claude Code, and it works.

All 7 hooks are stored in one .claude/settings.json file in the project root. Each entry has an event type, a tool filter (matcher), and a shell command. Exit code 2 rejects a matching PreToolUse call before execution. In PostToolUse, the action has already occurred: the hook reports feedback and cannot undo it.

For most hook events, only exit code 2 blocks an action. Claude Code treats exit code 1 as a non-blocking error and continues the action, even though 1 conventionally means failure in Unix.

— Anthropic, hooks reference, https://code.claude.com/docs/en/hooks

exit 0 means success. exit 1 means an error, but Claude continues (this is the first antipattern below). exit 2 has event-specific behavior: it can block PreToolUse, while PostToolUse only reports feedback about an action already taken.

Hook 1. Guard direct .env edits (PreToolUse, exit 2)

Blocks attempts to directly edit any file matching .env, .env.local, .env.production, and so on.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '(^|/)\\.env(\\.|$)' && { echo 'Do not edit `.env` directly. Tell me which variable to add, and I’ll open the file myself.' >&2; exit 2; } || exit 0"
          }
        ]
      }
    ]
  }
}

What it does: Before every Edit/Write/MultiEdit, it gets the file path, checks with grep whether it matches the .env pattern, and if so, returns exit 2 with a message. Claude sees stderr and tells me: “I tried to edit .env, but the hook blocked me; tell me which variable to add.”

Hook 2. Run prettier automatically after every code edit (PostToolUse, exit 0)

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '\\.(ts|tsx|js|jsx|json)$' && npx prettier --write \"$FILE\" 2>/dev/null & exit 0"
          }
        ]
      }
    ]
  }
}

What it does: After every Edit/Write, it gets the path and, if the extension is .ts/.tsx/.js/.jsx/.json, runs prettier in the background with &. It always exits 0 because this is formatting, not a restriction. If prettier fails, it’s no big deal; the file stays as it was.

The difference from hook 1 is fundamental: PreToolUse + exit 2 is a hard block before the action. PostToolUse + exit 0 is a soft edit after the action. These are two different modes for different tasks.

Hook 3. Block git push --force to the main branch (PreToolUse, exit 2)

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "CMD=$(jq -r '.tool_input.command // empty'); [ -z \"$CMD\" ] && exit 0; echo \"$CMD\" | grep -qE 'git[[:space:]]+push' && echo \"$CMD\" | grep -qE '(\\-\\-force|\\-\\-force-with-lease|[[:space:]]-f([[:space:]]|$))' && echo \"$CMD\" | grep -qE '([[:space:]/:])main([[:space:]/:]|$)' && { echo 'Force push to main is forbidden. Create a new branch or ask me to do it manually.' >&2; exit 2; } || exit 0"
          }
        ]
      }
    ]
  }
}

What it does: On every Bash launch, it checks that the command includes push, includes --force/--force-with-lease/-f, and mentions main. If all three conditions match, it blocks with a clear message.

I used to have this rule in CLAUDE.md in all caps. Claude still broke it about once every two weeks when I asked it to “force push to undo my bad commit.” This hook catches the literal command forms shown, but aliases, scripts, an implicit destination branch, or another Git client can bypass that string check. Enforce main-branch protection on the remote and limit the credentials available to the agent.

Hook 4. Block em dashes in text files (PostToolUse, exit 2)

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '\\.(md|mdx|tsx?)$' && grep -q $'\\xe2\\x80\\x94' \"$FILE\" && { echo \"File $FILE contains an em dash (U+2014). Replace it with a regular hyphen (U+002D).\" >&2; exit 2; } || exit 0"
          }
        ]
      }
    ]
  }
}

What it does: PostToolUse with exit 2 reports a post-check failure to Claude after the edit. It does not roll back or prevent the write. If Claude edited a file and left an em dash, the hook fails, Claude gets feedback, and tries again. After 2–3 sessions, Claude stops inserting this character altogether.

Note that grep searches specifically for the em dash character (U+2014) using $'\xe2\x80\x94', the Bash notation for writing it as UTF-8 bytes. If you simply write grep -q '-', it will catch regular hyphens and run on every file. An invisible character breaks the whole logic.

Hook 5. Free port 3000 before npm run dev (PreToolUse, exit 0)

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "CMD=$(jq -r '.tool_input.command // empty'); [ -z \"$CMD\" ] && exit 0; echo \"$CMD\" | grep -qE '(npm|pnpm|yarn)[[:space:]]+run[[:space:]]+dev' && { lsof -ti:3000 | xargs -r kill -9 2>/dev/null; echo 'Port 3000 freed before launch' >&2; }; exit 0"
          }
        ]
      }
    ]
  }
}

What it does: Before every npm run dev (or pnpm/yarn), it kills the process on port 3000. It always exits 0 because this is preparation, not a restriction. This protects against a class of traps where Claude sees “port in use,” thinks there’s a code problem, and starts fixing things, when in fact the old server was simply left running.

Hook 6. Auto-commit after successful tests (PostToolUse, exit 0)

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "INPUT=$(cat); CMD=$(printf '%s' \"$INPUT\" | jq -r '.tool_input.command // empty') || exit 1; EXIT=$(printf '%s' \"$INPUT\" | jq -r '.tool_response.exit_code // empty') || exit 1; case \"$CMD\" in 'npm test'|'pnpm test'|'yarn test') ;; *) exit 0 ;; esac; [ \"$EXIT\" = \"0\" ] || exit 0; git diff --cached --quiet || { echo 'Existing staged changes: review manually' >&2; exit 0; }; git add -- src/orders.ts tests/orders.test.ts || exit 1; git diff --cached --quiet && exit 0; git commit -m 'tests: checkpoint after successful run'"
          }
        ]
      }
    ]
  }
}

What it does: It reads JSON stdin once and accepts only the exact plain test commands shown. It proceeds only when the supplied tool_response.exit_code is explicitly zero; if your installed version does not expose that field, it skips the commit rather than guessing success. Replace src/orders.ts and tests/orders.test.ts with the exact files owned by the task. Existing staged changes stop the recipe so unrelated work is not included. A failed add stops it, an empty diff creates no commit, and normal commit hooks remain enabled. Test successful, failed, and missing-result inputs offline before enabling it.

Antipattern: adding git commit --no-verify to every Edit. Keep commit checks enabled. If a check triggers this automation recursively, narrow the trigger or redesign that check; do not bypass verification by default. Review the staged diff and task ownership before enabling automatic commits.

Hook 7. Inject fresh context at session start (SessionStart with additionalContext)

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo 'no-branch'); STATUS=$(git status --short 2>/dev/null | head -10); echo \"{\\\"additionalContext\\\":\\\"Current branch: $BRANCH. Uncommitted changes:\\n$STATUS\\\"}\""
          }
        ]
      }
    ]
  }
}

What it does: At session start, it collects the current Git branch and list of uncommitted changes, then returns JSON with an additionalContext field. Claude Code injects this text into the first system message, so Claude knows from the beginning where it is and what remains from the previous session.

Use additionalContext for information Claude should know about the current state of the environment or a just-completed operation: environment state (current branch, deployment target, active feature flags), conditional project rules (which test command applies to the file just edited), external data (open issues assigned to you, recent CI results).

— Anthropic, hooks reference, https://code.claude.com/docs/en/hooks

Extensions to this hook: fetch the latest commit and its description, GitHub Actions status, the active sprint from Linear/Jira, and open issues for the current repository.

3 hook pitfalls that catch every other person

Key takeaway

The first pitfall: a hook with exit 1 passes silently, and Claude ignores the error. The second: heavy work in a hook is killed after a 60-second timeout. The third: a hook sees only the environment variables from ~/.zshrc, not the project’s .env. Each is fixed with one line, but without knowing about these pitfalls, it can seem like “hooks don’t work.”

Pitfall 1. A hook with exit 1 passes silently

The most dangerous one. For a week, I thought my .env protector worked, but it turned out I had jq -r '.tool_input.file_path' in the hook without checking for null. When tool_input was empty, jq exited with code 1. Claude treated exit 1 as “hook error, continue,” so the action went through. I couldn’t understand why .env was still being touched. I only saw the cause when I ran the hook manually in a terminal with empty input.

The fix is one rule: start every hook by checking for null, and end it with || exit 0. Any unexpected exit then results in a “safe” exit rather than silently passing through.

# Safe start and end template:
FILE=$(jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] && exit 0
# ... your logic ...
exit 0   # or || exit 0 on one line

Pitfall 2. Slow hooks are killed by a timeout

I had a hook that ran eslint --fix on the whole file after Edit. Large files (1000+ lines) took 8–10 seconds to process. Claude Code’s default hook timeout is 60 seconds, so I thought I was within the limit. But when several Edits happen in one minute, hooks accumulate and queue up, and some are simply skipped. I realized this only when the log showed “hook timed out.”

Fix: if a hook requires heavy work, run it in the background. eslint --fix "$FILE" & without waiting for the result. Then the hook returns exit 0 in milliseconds while the real work runs in the background.

# Heavy operation in background:
npx eslint --fix "$FILE" &
exit 0

Pitfall 3. A hook cannot see project environment variables

I thought process.env in a hook would be available as it is in a Node.js app. It isn’t. The hook runs in a shell spawned by the Claude Code parent process and sees only what’s in ~/.zshrc or ~/.bashrc, plus what Claude Code explicitly passes through. If your hook depends on OPENAI_API_KEY from .env, you’ll have to read .env manually.

Fix: explicitly read .env in the hook command.

# Passing project env into hook:
cd "$PWD" && source .env && curl -H "Authorization: Bearer $OPENAI_API_KEY" ...
# Or with dotenv:
dotenv -e .env -- node my-validator.js

This is especially painful in a monorepo, where different packages have their own .env files. Solve it by passing through PWD and explicitly running cd "$PWD" at the start of the command.

Claude Code hooks best practices: how do you test before committing?

Key takeaway

A hook is a shell command. You can run it manually in a terminal with fake stdin, check its exit code and stderr, and do all that before Claude Code runs it. This saves hours of debugging and protects you from hooks that block their own operation.

Basic test scenario. Take the hook command from the JSON, open a terminal, and feed it the JSON Claude Code would send:

# Test hook 1 (protect .env):
echo '{"tool_input":{"file_path":".env"}}' | bash -c '
FILE=$(jq -r ".tool_input.file_path // empty")
[ -z "$FILE" ] && exit 0
echo "$FILE" | grep -qE "(^|/)\.env(\.|$)" && {
  echo "BLOCKED" >&2
  exit 2
} || exit 0
'
echo "Exit code: $?"

You should see BLOCKED in stderr and Exit code: 2. If you get exit 0, your regex isn’t working; fix grep.

Scenarios to check:

  1. Happy path - the file should be blocked. .env for hook 1, git push --force main for hook 3.
  2. Negative path - the file should not be blocked. README.md for hook 1, git push origin feature-branch for hook 3.
  3. Edge cases - empty input, null values, unexpected characters. The hook should either return exit 0 or fail with a clear error, not silently pass through.

Where to put experimental hooks. Not in .claude/settings.json, but in .claude/settings.local.json (not in git). Put new hooks there under PostToolUse with echo instead of the real action, check whether they fire, then move them into the production settings.json.

// Experiment in settings.local.json - writes to /tmp/hook-debug.log:
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "hooks": [
          {
            "type": "command",
            "command": "echo \"[hook fired] $(date) $(jq -r '.tool_input.file_path // \"?\"')\" >> /tmp/hook-debug.log"
          }
        ]
      }
    ]
  }
}

After 5–10 edits in Claude Code, open /tmp/hook-debug.log to see which files triggered the hook. If it’s empty, the matcher doesn’t fit; fix the regex.

What’s next?

Key takeaway

Start with PostToolUse hooks that exit 0; they’re the safest and don’t block anything. After a week, add your first PreToolUse hook with exit 2 to protect .env or prevent force-pushing. After two weeks, CLAUDE.md will shrink from a 200-line contract to a short 50–80-line context description, with selected event checks moved into hooks and hard access limits enforced separately by permissions, sandboxing, and remote protections.

5 steps for the coming week:

  1. Grep your CLAUDE.md for words like “never,” “forbidden,” “always,” and “must.” Each such rule is a candidate for a hook. If a rule is imperative, the text file won’t enforce it.
  2. Start with PostToolUse + exit 0. These are the safest: they make a change but don’t block anything. Prettier (hook 2), killing the dev server (hook 5), auto-commit after tests (hook 6).
  3. Only when you’re confident, add your first PreToolUse + exit 2. For example, protect .env (hook 1) or prevent force-pushing (hook 3). This is a hard block, and a mistake in the grep command can block you yourself.
  4. Test hooks manually before committing (see the section above). It saves hours of debugging.
  5. Keep settings.local.json for experiments. Put new hooks there with echo instead of the real action.

Once your hooks are configured, the next step in the cluster is how to undo changes in Claude Code, in case a hook lets something dangerous through. If you want Claude to remember your project structure between sessions, build a Second Brain in Claude Code (business/ folder + linked knowledge base). And to give your project’s skills names so they can be reused, see Claude Code Skills.

If you need access to neural networks through Ailoot, take a look at Ailoot.

More details

Questions about Hooks in Claude Code 2026: step-by-step setup and 7 ready-to-use examples

Why must a hook read its JSON input only once?

Stdin is a stream: the first jq process consumes it, leaving later reads at EOF. Save the input once, then extract command and result fields from that saved value. Treat an absent or unknown result as unverified and skip auto-commit.

Do the example regex hooks create a security boundary?

No. They check specific events and recognizable command strings. Shell redirection, aliases, scripts, MCP tools, or another write path may bypass them; use restricted credentials, filesystem permissions, sandboxing, and protected branches for hard limits.

All articles