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:

  1. Why read hooks as lifecycle slots rather than script lists?
  2. How does Codex separate visible hook entries from runnable handlers?
  3. Why does a hook run preview before it emits started/completed events?
  4. What can the prompt, tool, approval, compact, and stop slots change?
  5. How is PermissionRequest different from approval and sandboxing?
  6. 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