Start with one ordinary turn. The user submits a prompt. Codex may prepare context, call the model, run shell commands, edit files, ask for approval, compact a long context, and eventually decide that the model no longer needs a follow-up request. Each of those points is attractive to local automation: inspect the prompt, block a risky command, answer an approval request, annotate a tool result, or ask the model to make one final check.
Calling all of that "hooks" is too loose for source reading.
Codex makes the boundary sharper. A hook belongs to a
specific event, and that event defines the request shape, parsed
output, and runtime consequence. A command hook attached
to PreToolUse can block a tool. The same style of
hook attached to PermissionRequest returns an approval
verdict. A Stop hook can push the turn back into the
model loop.
The useful model is this: Codex hooks are typed
lifecycle slots. They are governed by
HookEventName, request schemas, event-specific
parsers, HookStarted/HookCompleted
events, and hook_runtime glue that reconnects
outcomes to the turn.
Evidence boundary. This article only describes behavior visible in the public openai/codex source: hook events, discovery, preview/run/parse, turn call sites, tool approval integration, compact integration, stop continuation, and protocol events. The policy implemented inside a user's hook command is outside this article's claim.
This part follows six questions:
- Why read hooks as lifecycle slots rather than script lists?
- How does Codex separate visible hook entries from runnable handlers?
- Why does a hook run preview before it emits started/completed events?
- What can the prompt, tool, approval, compact, and stop slots change?
- How is
PermissionRequestdifferent from approval and sandboxing? - Why does this prepare the next part on performance and prompt cache?
1. Put Hooks Back on the Timeline
The first source-level object is not a config file; it is the
event name. The protocol-level HookEventName enum
lists PreToolUse, PermissionRequest,
PostToolUse, PreCompact,
PostCompact, SessionStart,
UserPromptSubmit, SubagentStart,
SubagentStop, and Stop. Read in order,
those names form a turn lifecycle.
| Slot | Where it fires | Bounded consequence | Owner to follow |
|---|---|---|---|
SessionStart / SubagentStart |
When a session or thread-spawned subagent starts. | Add model context; SessionStart can also stop the turn. |
session_start.rs and run_pending_session_start_hooks. |
UserPromptSubmit |
Before user input is accepted into history. | Add model context or prevent that input from continuing. | user_prompt_submit.rs and inspect_pending_input. |
PreToolUse |
Before a tool executes. | Block the tool, add context, or rewrite hook-visible input. | pre_tool_use.rs and tools/registry.rs. |
PermissionRequest |
Inside the approval path, before Guardian or user UI. | Return Allow, Deny, or no decision. |
permission_request.rs and the tool orchestrator. |
PostToolUse |
After a tool has produced a successful result. | Add context, provide model-visible feedback, or stop with feedback. | post_tool_use.rs and PostToolUseFeedbackOutput. |
PreCompact / PostCompact |
Around remote compaction. | Let compact continue or interrupt compact/turn. | compact.rs and compact_remote_v2.rs. |
Stop / SubagentStop |
When the runtime thinks the turn can finish. | Stop, continue, or add continuation fragments for another model pass. | stop.rs and run_turn_stop_hooks. |
The table gives the core intuition: hooks are not a generic loop
outside the turn. They sit at precise boundaries. A context-capable
slot writes a ContextualUserFragment. A tool slot
acts on one tool call. A permission slot answers one permission
request.
A shell command makes the separation concrete. If the model wants
to run npm test, the command crosses three adjacent
boundaries:
tool call: shell("npm test")
-> PreToolUse sees tool name and input; may block or add context
-> PermissionRequest sees the approval payload; may allow or deny
-> ShellRuntime executes under the selected sandbox
-> PostToolUse sees the result; may add feedback for the next model step
That is why a hook should not be read as one universal
interceptor. PreToolUse decides whether the call may
enter the handler. PermissionRequest answers one
approval request. PostToolUse changes how the result
returns to the model view.
2. Discovery Is Wider Than Runtime Authority
Part VII showed that a plugin manifest can declare
hooks. Being declared, however, is not the same as
being runnable. Codex separates discovery from execution.
ClaudeHooksEngine::new receives hook enablement,
trust bypass state, the config layer stack, plugin hook sources,
plugin hook load warnings, and the command shell. When hooks are
enabled, it calls discover_handlers. The discovery
result has two different surfaces: HookListEntry for
user-visible metadata and ConfiguredHandler for
runtime execution.
| Structure | What it carries | Reason for the split |
|---|---|---|
HookListEntry |
Event, matcher, command, source, plugin id, enabled state, current hash, trust status. | The UI can show hooks that are disabled, untrusted, or modified. |
ConfiguredHandler |
Event, matcher, command, timeout, source path, source, display order, env. | The runtime only keeps handlers that may execute in this session. |
The filter is explicit. Discovery reads config layers and plugin
sources, validates matchers, skips empty commands and unsupported
async hooks, computes a command hook hash, then combines enablement
with HookTrustStatus. Managed hooks can run as managed.
Non-managed hooks become runnable only when the current hash
matches a trusted hash, unless the runtime has been told to bypass
trust.
The protected invariant is practical: visible automation is not automatically executable automation. A project or plugin can contribute hook metadata without silently becoming active code.
3. A Hook Run Is a Visible Turn Fact
When a hook event fires, Codex does not run the command silently.
Each event module follows the same broad shape: preview matching
handlers, convert them into
HookRunSummary, execute the
commands, then parse stdout, stderr, exit code, or JSON output into
an event-specific outcome.
dispatcher::running_summary builds the
Running record. hook_runtime emits
EventMsg::HookStarted. After command execution,
emit_hook_completed_events records telemetry and
analytics, then emits EventMsg::HookCompleted. A
client does not have to infer hook progress from terminal output;
it receives typed events with source, status, timing, and entries.
pub struct HookRunSummary {
pub id: String,
pub event_name: HookEventName,
pub handler_type: HookHandlerType,
pub execution_mode: HookExecutionMode,
pub scope: HookScope,
pub status: HookRunStatus,
pub started_at: i64,
pub completed_at: Option<i64>,
pub entries: Vec<HookOutputEntry>,
}
This is the useful subset of HookRunSummary. It turns
“a script ran” into event name, handler kind, execution mode, scope,
status, timing, and structured output entries. A hook run is therefore
a turn fact that app-server, TUI, telemetry, and rollout policy can each
handle deliberately, not a stray terminal log.
Why preview? The UI needs to show which hooks are running before a long hook finishes. Without preview, slow automation would look like an unexplained runtime pause.
4. Prompt Gate: Before Input Becomes History
Inside run_turn, Codex runs pre-sampling compaction,
records context updates, resolves skill/plugin injections, then
runs pending session-start hooks and prompt-submit hooks before it
samples the model.
inspect_pending_input builds a
UserPromptSubmitRequest with session id, turn id, cwd,
transcript path, model, permission mode, and prompt. If the hook
stops processing, the input can be rejected before becoming
durable conversation history. If the hook adds context,
record_additional_contexts converts it into a
HookAdditionalContext developer message.
This is different from skill injection. A skill is selected working guidance that is recorded as turn input. A prompt hook is a gate before user input is accepted; it can annotate or block that input at the boundary.
5. Tool Gates: Before, During, and After Approval
Tool hooks are easiest to confuse because several events surround
one tool call. The source keeps the layers separate:
PreToolUse, PermissionRequest, and
PostToolUse answer different questions.
5.1 PreToolUse: Local Rewrite or Block Before Execution
In tools/registry.rs, after the invocation payload has
passed basic compatibility checks, the registry calls
run_pre_tool_use_hooks. The hook request carries the
canonical tool name, matcher aliases, tool use id, and tool input.
Handler selection can use compatibility aliases, but stdin keeps
the canonical tool_name for audit stability.
PreToolUseOutcome has three meaningful consequences.
should_block prevents execution and returns a message
to the model. additional_contexts enters model
context. updated_input can rebuild the invocation; if
the hook blocks, the updated input is ignored.
5.2 PermissionRequest: A First Answer in the Approval Path
PermissionRequest fires later. When policy requires
approval and this path evaluates permission hooks, the tool
orchestrator calls run_permission_request_hooks. This
hook does not rewrite input. It returns an approval verdict:
Allow, Deny, or no verdict.
The fold rule is conservative. Any Deny wins. If no
handler denies, an Allow can approve the request. If
no handler decides, the normal Guardian or user approval path
continues. This still does not replace sandboxing. Approval answers
whether Codex may attempt the side effect; the execution attempt
remains constrained by the permission profile, sandbox, and tool
runtime.
5.3 PostToolUse: Model-Visible Feedback After Execution
After a successful tool result, the registry may call
run_post_tool_use_hooks. The request includes tool
input and tool response. A post-tool hook can add context, return
feedback, or stop with feedback.
The important detail is model visibility. If the hook returns
feedback, the registry wraps the original result in
PostToolUseFeedbackOutput. The original result is
preserved inside the output object, but the next model-visible
message can become the hook feedback.
6. Compact and Stop Are Lifecycle Boundaries Too
Hooks also appear at context pressure and turn completion.
Around remote compaction, compact_remote_v2.rs calls
run_pre_compact_hooks and
run_post_compact_hooks. Their outcome is intentionally
narrow: continue, or interrupt compact/turn. They do not invent
compaction strategy or rewrite summaries.
Stop is more subtle. run_turn calls
run_turn_stop_hooks only after the model no longer
needs follow-up, there is no pending input, and the token state
does not force continuation. The runtime is ready to finish, but a
stop hook can still return should_block with
continuation_fragments.
If Codex can build a HookPromptFragment, it records a
hook prompt message, marks stop_hook_active, and
re-enters the model loop. A stop hook sits before completion is
finalized; it is the final chance to ask for one more bounded
model pass.
The closer a hook sits to a side effect, the narrower and more concrete its authority becomes. The closer it sits to input or completion, the more it affects model context and continuation. Codex turns a broad extension idea into small lifecycle gates.
7. Common Misreadings
The main source-reading risk is collapsing adjacent boundaries. Hook, approval, sandbox, and event projection all sit near safety and observability, but they are owned by different runtime layers.
| Misreading | Better reading | Consequence |
|---|---|---|
| The hook list is the runnable handler list. | Entries can be disabled, untrusted, or modified; runtime handlers pass trust filtering. | Confuses discovered automation with executing automation. |
PreToolUse is approval. |
PreToolUse blocks or rewrites input; PermissionRequest returns allow/deny. |
Treats validation as authorization. |
| Approval bypasses sandboxing. | Approval permits an attempt; sandboxing still constrains the process. | Overstates what an allow decision grants. |
PostToolUse only logs. |
It can provide model-visible feedback before the next sampling request. | Misses the last interpretation layer after a tool result. |
Stop is an after hook. |
It runs at the completion boundary and can add continuation fragments. | Misses a final controlled model pass. |
8. Why Performance and Prompt Cache Come Next
With hooks placed on the lifecycle, the first eight parts now form a complete runtime route. User input enters a turn. Context and extensions construct the model-visible view. The tool surface defines what the model may request. Hooks, approval, and sandboxing govern side effects. Events project facts to clients. Compact and stop logic decide how long work continues.
That is the right point to discuss performance and prompt cache. Codex performance is not one faster function. It depends on which prompt prefix stays stable, which skill and tool descriptions enter only on demand, whether hook-provided context changes the model view, how compaction reshapes history, and whether event projection keeps the user oriented while the turn is still running. The next part can now talk about prompt cache without reducing the system to a single cache-hit number.
Source References
- Pinned openai/codex source snapshot
- EventMsg includes HookStarted and HookCompleted
- HookEventName event list
- HookRunSummary, HookStartedEvent, and HookCompletedEvent
- ConfiguredHandler, HookListEntry, and ClaudeHooksEngine
- Hook engine preview and run entry points
- discover_handlers reads config and plugin hook sources
- append_matcher_groups, trust status, and runnable handler filtering
- Handler selection, running summary, and concurrent execution
- HookScope for thread and turn events
- Hook call sites inside run_turn
- run_hooks_and_record_inputs checks hooks before recording input
- run_pending_session_start_hooks
- run_pre_tool_use_hooks
- run_permission_request_hooks
- run_post_tool_use_hooks
- run_turn_stop_hooks
- pre/post compact hooks
- run_context_injecting_hook, record_additional_contexts, and hook events
- SessionStart and SubagentStart request/outcome
- UserPromptSubmit request/outcome
- PreToolUse request/outcome and updated input resolution
- PermissionRequest hook contract and deny-wins fold
- PostToolUse request/outcome
- Stop and SubagentStop request/outcome
- Stop hook continuation fragment aggregation
- Tool registry pre/post hook integration
- Tool orchestrator gives permission-request hooks first answer
- Hook gates around remote compact
- PluginManifest paths include hooks
- PluginLoadOutcome exposes effective plugin hook sources