Thirty-Three Hook Events or Three Approval Policies: Matching the Instrument to the Rule
The expressive gap between the two automation models is real. It is also almost entirely ceiling, and you will spend your time on the floor.
Thirty-three named events against a handful of scopes
The two agents automate themselves at different resolutions, and the gap is wide enough to change what kinds of policy you can express.
Claude Code's hook system fires on named lifecycle events, and the list is
long. Per-session: SessionStart, SessionEnd,
Setup. Per-turn: UserPromptSubmit,
UserPromptExpansion, Stop,
StopFailure. Around every tool call:
PreToolUse, PermissionRequest,
PermissionDenied, PostToolUse,
PostToolUseFailure, PostToolBatch. Around
delegated work: SubagentStart, SubagentStop,
TaskCreated, TaskCompleted,
TeammateIdle. Around context and configuration:
InstructionsLoaded, ConfigChange,
CwdChanged, DirectoryAdded,
FileChanged, WorktreeCreate,
WorktreeRemove. Around compaction and models:
PreCompact, PostCompact,
PreModelSwitch, PostModelSwitch. Plus
Notification, MessageDisplay,
Elicitation and ElicitationResult.
Codex approaches automation from the other end. Its primary levers are
coarse and declarative: an approval_policy that takes
untrusted, on-request or never, and a
sandbox_mode that takes values including
workspace-write and danger-full-access, selectable
as named profiles. Lifecycle hooks arrived later, configured from a
hooks.json or an inline [hooks] block and browsable
from /hooks.
The argument here is that the fine-grained model is genuinely more expressive, that almost nobody uses more than a fraction of it, and that the choice between the two is not about which is more powerful but about whether your policy is a rule about a specific tool call or a statement about the whole session. Those need different instruments, and reaching for the wrong one is how teams end up with hook scripts nobody can explain.
What only the fine-grained model can do
There is a class of control that requires an event at the tool boundary, and no amount of session-level configuration substitutes for it.
The canonical case is a deterministic veto on a specific command shape. A
PreToolUse hook matched to Bash can inspect the
command the model is about to run and return a decision that blocks it,
carrying a reason the agent sees. That is not a prompt asking a human, and it
is not a sandbox that would have caught it anyway; it is a rule that executes
identically every time, on a decision the model would otherwise have made for
itself.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(rm *)",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh"
}
]
}
]
}
}
The matcher semantics are worth knowing because they are quietly
load-bearing. A matcher of only letters, digits, underscores, hyphens, spaces,
commas or pipes is an exact string or a pipe-separated list —
Bash, Edit|Write. Anything containing other
characters is treated as an unanchored JavaScript regular expression, so
mcp__memory__.* matches a family of tools and
^Notebook anchors deliberately. Adding a dot to a matcher you
believed was a literal silently converts it into a pattern.
The second thing only the fine-grained model gives you is scope. Hooks can
be declared in ~/.claude/settings.json for every project you
touch, in a committed .claude/settings.json for one repository, in
a gitignored .claude/settings.local.json for you alone, in
managed policy settings an administrator controls, in a plugin, in skill
frontmatter for the rest of a session, or in subagent frontmatter for the
lifetime of that subagent. That layering is the difference between a personal
convenience and an organisational control, and it is expressed in where the
file lives rather than in what it says.
What the coarse model gets right
Set against thirty-three events, three approval policies and a handful of sandbox profiles look impoverished. They are not, for a reason worth stating plainly: most of what teams actually want from hooks is a statement about the whole session, and a session-level setting says it in one line that a new team member can read.
Consider what people usually configure. Run the formatter after edits. Run
the type checker before finishing. Do not touch these paths. Do not reach the
network. Ask before anything destructive. Of those five, three are naturally
session-wide and two are path rules. None of them requires knowing that
PostToolBatch exists.
The coarse model also fails more legibly. A sandbox profile that denies
network access denies it to everything, including the case you did not
anticipate, and it does so at a boundary below the model. A
PreToolUse hook that was supposed to catch destructive commands
catches exactly the shapes its author thought of, and the gap between the
rule and the intent is invisible until something slips through it.
Comparisons written in the first half of 2026 list Codex as having no hooks
at all, and they were right when written. By August 2026 lifecycle hooks are
documented as configurable from hooks.json or an inline
[hooks] block. The direction of travel is convergence, and any
claim in this area — including this article's — should be checked
against both vendors' current documentation rather than trusted at second
hand.
Two events out of thirty-three
In practice the distribution of hook usage is extremely skewed.
PreToolUse and PostToolUse account for nearly
everything anybody writes: block the dangerous thing, run the formatter after
the edit. A third tier — SessionStart to load context,
PreCompact to preserve something before it is summarised away
— shows up in perhaps one repository in ten. The rest are almost
entirely unused, not because they are badly chosen but because most teams do
not have a policy that needs them.
That has a consequence for how you should read the feature comparison. A table showing thirty-three events against a smaller number is measuring ceiling, not floor, and the floor is where you will live. If your policy is two rules, the tool with two hundred events and the tool with six will both express it, and the one with six will express it in fewer lines that more people can read.
The ceiling matters when your policy is not two rules. Auditing every subagent spawn, reacting to a model switch mid-session, capturing state before compaction, refusing a prompt expansion — these are real requirements for teams operating agents at scale, and they need events that exist.
Where the two models genuinely diverge: enforcement point
Underneath the granularity question sits a more consequential one, which is where the enforcement happens.
A hook is application-layer. It runs because the agent's own harness chose to call it, and it constrains what that harness will do. A sandbox profile is enforced below the application, by the operating system: Seatbelt on macOS, Landlock on Linux, restricted tokens and filesystem ACLs on Windows. That distinction is not a matter of taste. An application-layer control is defeated by anything that gets outside the application — a subprocess, a wrapper script, a tool the harness does not route through the hook — and a kernel control is not.
Both products now offer both kinds. The relevant question for a given rule
is which layer it belongs at, and the honest answer is usually the lower one.
If the rule is "this process must not reach the network", express it as
egress policy rather than as a hook that inspects commands for
curl, because the hook is a list of spellings and the network
policy is a property of the environment.
Teams write a PreToolUse hook that greps for dangerous command
strings, then treat it as a security control. It is a typo guard. It stops the
model doing the obvious wrong thing, which is worth having, and it stops
nothing that is trying. Anything you would describe to an auditor as a control
needs to be enforced below the agent.
Choosing an instrument for a given rule
A rule of thumb that survives both products and most disagreements about them: match the instrument to what the rule is a statement about.
If the rule is a property of the environment — what may be reached, what may be written, what credentials exist — it belongs in the sandbox or the network policy, on either tool, and expressing it as a hook is a downgrade.
If the rule is a property of a specific tool call — this command shape, this path, this MCP server — it belongs in a fine-grained hook, and this is where the thirty-three-event model earns its complexity.
If the rule is a property of the whole session — how much autonomy this run gets, whether it may act unattended — it belongs in an approval policy or a profile, and writing it as a hook means reimplementing a mode that already exists.
If the rule is a property of the work rather than the tooling — this must be reviewed, this must pass tests before merging — it belongs in CI, where it applies to human commits too and cannot be turned off by switching agents.
rule instrument (either product)
---------------------------------------------------------
no network from this run sandbox / egress policy, not a hook
never rm -rf without asking fine-grained pre-tool hook
unattended overnight run approval policy / profile
nothing merges unreviewed CI, not the agent at all
The maintenance cost nobody budgets
Hooks are code that runs on every relevant event, and they inherit every
property of code: they break, they get slow, they encode assumptions that
stop being true. A hook that shells out on every PostToolUse adds
its runtime to every edit, and a formatter invoked that way on a large file
is a delay the user experiences as the agent being slow.
They also fail in a direction people do not expect. A hook that errors is usually treated as no decision, which means a control you believe is active can be silently inert because a path changed. If you rely on one, test that it still blocks — deliberately, on a schedule — the same way you would test a backup.
The coarse model has less of this exposure simply because there is less of it. That is not an argument that granularity is bad. It is an argument that every event you hook is a small ongoing obligation, and that a team choosing the more expressive tool should hook the two events it needs rather than the thirty-three it now can.
The hook event names, the settings file scopes and the matcher evaluation
rules are from
Anthropic's Claude Code hooks
documentation. The approval_policy and
sandbox_mode values, and the hooks.json and inline
[hooks] configuration, are from
OpenAI's
Codex configuration documentation. The observation that Codex had no hook
system in mid-2026 is from
Firecrawl (June
3, 2026); the kernel-level sandbox mechanisms named there —
Seatbelt, Landlock, Windows restricted tokens — are corroborated by the
Wikipedia
record. The claim that two events account for most real usage is my own
observation from repositories I have worked in, not a measured figure.
Claude Code exposes thirty-three named lifecycle events with layered configuration scopes; Codex leads with three approval policies and a few sandbox profiles, and has since added its own lifecycle hooks. The expressive gap is real, but almost all actual usage is two events, so the comparison measures ceiling rather than floor. Choose the instrument by what the rule is a statement about: environment rules belong in the sandbox, tool-call rules in a fine-grained hook, session autonomy in an approval policy, and anything you would call a control belongs below the agent rather than inside it — because a hook is application-layer, and application-layer controls are defeated by anything that gets outside the application.