Skip to content
← Inforjota
Cursos Inforjota Português

Lesson 3 of 7 · 15 min · Checked on 10 October 2026

Hooks: automatic checks the agent cannot skip

By the end of this lesson you will be able to:

  • Explain what a hook is and at which moments it runs
  • Create a hook that stops the agent from editing a folder, and test it by hand before turning it on
  • Connect the same hook to Claude Code and to Codex

In the hardware shop, Mr Manuel stuck a note on the cash drawer: “Only the owner opens this.” The assistant reads the note and obeys. One very busy Saturday, he forgets. The next day, Mr Manuel fits a lock.

With an agent it is the same. In AGENTS.md (lesson 1/07) you wrote “do not touch the privado/ folder” (privado means “private”). Almost always it obeys. In a long session, one day, it edits a file in there.

The note is the rule in AGENTS.md. The lock is a hook.

The concept

A hook is a command that the tool itself runs, on its own, at fixed moments. It is not the model that decides whether it runs: it is the program. The most used moments have the same name in Claude Code and in Codex:

  • PreToolUse: before the agent uses a tool (editing a file, running a command).
  • PostToolUse: after it uses it.
  • Stop: when the agent finishes answering.

The hook receives a description of what the agent is about to do (in JSON, a text format with fields and values) and answers with an exit code. If it ends with 0, the action goes ahead. If it ends with 2, the action is blocked, and the message the hook wrote is handed to the agent, so it knows why. Only hooks that run before (PreToolUse) can block: a PostToolUse arrives after the action has happened and is for reacting, not preventing.

Three common uses

  • Protect a folder. A PreToolUse hook sees the file path and blocks if it is inside privado/. This is the complete example in this lesson.
  • Format after every edit. A PostToolUse hook runs a formatter (a program that tidies code the same way every time) on the file that just changed. The official Claude Code guide has a ready-made example.
  • Tests before finishing. A Stop hook runs python3 -m pytest (lesson 2/01). If the tests fail, it returns 2 with the error and the agent keeps working.

Watch out

A badly made hook blocks everything. If the script always returns 2, the agent cannot edit any file. A Stop hook that always fails does not let the agent stop: the Claude Code guide tells the script to check the stop_hook_active field so it does not go into a loop. And a hook runs with your permissions, on your computer. Never copy hooks from other people without reading them, and try them first in experiencias (“experiments”).

How it is done today: the minimal hook

This is how it looks on 6 October 2026; your screen may differ. Look for the button with the same name.

The script is the same for both tools. Save it as .claude/hooks/proteger.py (proteger means “protect”):

# Brake: stops the agent from touching the privado/ folder
import json
import sys

dados = json.load(sys.stdin)
entrada = dados.get("tool_input", {})
alvo = str(entrada.get("file_path", "")) + " " + str(entrada.get("command", ""))
alvo = alvo.replace("\\", "/")

if "privado/" in alvo:
    print("Blocked: the privado/ folder is not to be touched.", file=sys.stderr)
    sys.exit(2)

sys.exit(0)

It reads the description of the action, joins the file path and the text of the command, swaps the Windows backslashes for slashes and looks for privado/. If it finds it, it writes the reason and exits with 2.

It is a text filter, not a barrier: PRIVADO\notas.txt, in upper case, gets through with 0, and a command that merely mentions privado/ without touching it is blocked with 2. That is enough to learn the mechanism. To protect a folder for real, normalise the path (lower case, os.path.realpath) and compare it with the full path of the protected folder.

Test it by hand before turning it on, with a bad case and a good one, inside the project folder:

echo '{"tool_name":"Write","tool_input":{"file_path":"privado/notas.txt"}}' | python3 .claude/hooks/proteger.py; echo $?
echo '{"tool_name":"Write","tool_input":{"file_path":"publico/notas.txt"}}' | python3 .claude/hooks/proteger.py; echo $?

The first must show the message and 2; the second, only 0. (notas means “notes” and publico means “public”.) In PowerShell, swap echo $? for echo $LASTEXITCODE and python3 for python.

Claude Code. Connect the hook in the project's .claude/settings.json file:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/proteger.py"
          }
        ]
      }
    ]
  }
}

The matcher chooses the tools being watched: here, editing and writing files. Type /hooks in the session to confirm the hook appears.

Codex. Copy the script to .codex/hooks/proteger.py and create .codex/hooks.json with the same content, changing only the command line:

"command": "python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/proteger.py\""

In Codex, edits arrive as apply_patch, with the text of the change in command; the script looks there too, so it works. Codex only loads hooks from a project you trust, and each new or changed hook has to be approved in /hooks.

We tested this hook in both tools: the edit in privado/ was blocked with the message, and the one in publico/ went through.

Tip

The Codex documentation calls hooks “a useful guardrail, not a complete boundary”. This one blocks edits, not terminal commands. Permissions (lessons 1/03 and 1/06) remain the other half of the protection.

Tip

Add a line to AGENTS.md explaining the rule. The agent then understands why it was blocked and does not waste time trying again.

Exercise

How I know it worked the agent says the edit in privado/notas.txt was blocked, with the hook's message. The diff shows only publico/notas.txt changed. If the agent asks to run a terminal command that writes in privado/, refuse.

If it didn't work

  • Nothing is blocked: confirm in /hooks that the hook appears. Check that the JSON does not have a stray comma at the end of a list. Restart the session. In Codex, approve the hook in /hooks.
  • Everything is blocked: close the session before you take the hook out of the configuration, repeat the hand test with publico/ and only then open it again. If it gives 2, the bug is in the script.
  • “python3: command not found” (Windows): swap python3 for python in the command line.
  • “No such file”: the path of the script is wrong; check the name of the hooks folder.

Sources:

Videos

Hooks no Claude Code na prática: 5 hooks com teste automatizado
OpsRoom · IA & Agentes de Código · 11 min · PT-BR · subtitles · Watch on YouTube
Claude Code hooks in practice: the contract of a hook, the map of events and hooks to protect sensitive files, add context to the prompt and signal the end of a task, with automated tests.
Hooks in Claude Code
Claude · 3 min · EN · subtitles · Watch on YouTube
An official Anthropic video, about three minutes: hooks for deterministic control of Claude Code, such as formatting after edits, blocking dangerous operations and sharing hooks with the team.

Test what you learned

1. You want the tests to always run before the agent says “I am done”, even on the days when it forgets. Where do you put that?
2. You have just written a new hook. What is the first test?
3. You turned on a PreToolUse hook and now the agent cannot edit any file, not even those in publico/. What do you do?

Corrections to this lesson (1)
  • : After a second external review: the example hook is a text filter (upper case and equivalent paths get through; a command that merely mentions `privado/` is blocked), with a note on normalising paths; only PreToolUse hooks block, PostToolUse arrives afterwards; when diagnosing, close the session before removing the hook.