Evidence Trail

Before the Prompt Lands: Codex and Gemini Turn Hooks Into Agent Control Planes

March 20, 2026 / Daily Edition / 2 source signals.

repo openai/codex main
2 source signals 2 repos 6fef421
> 6fef421 / March 20, 2026 / Daily Edition
Read Story Open Edition

Reporter Notes

Notes — 2026-03-20

Selected angle

**Before the prompt lands, both Codex and Gemini now expose a real control plane for shaping a turn.**

This story focuses on pre-turn interception, not tool concurrency. That keeps it clearly distinct from 2026-03-19's article on parallel tool scheduling.

Candidate angles considered

1. **Picked:** pre-turn hook interception as agent middleware

2. Approval memory inheritance across subagents / policy layers

3. CLI-to-runtime decoupling via app-server / AgentLoopContext plumbing

Why this angle won

  • Different layer of the stack from yesterday: prompt ingress, not tool execution
  • Strong, recent Codex evidence from a Mar 17 hook addition
  • Strong Gemini counterpoint from existing docs and core client code
  • Human-readable framing: middleware before the model sees the turn

Key evidence

Codex

  • Commit 6fef42165 adds UserPromptSubmit hooks.
  • codex-rs/hooks/src/events/user_prompt_submit.rs
  • serializes a dedicated hook input with session, cwd, transcript path, model, permission mode, and prompt
  • hook output can stop, block, or add extra context
  • plain-text stdout can also become additional context
  • codex-rs/core/src/hook_runtime.rs
  • inspect_pending_input() runs UserPromptSubmit before pending user input is accepted
  • accepted input is later recorded with record_pending_input()
  • extra context is transformed into DeveloperInstructions
  • codex-rs/core/src/state/turn.rs
  • turn state explicitly buffers pending_input, making the interception window visible in runtime state

Gemini CLI

  • docs/hooks/index.md
  • hooks run synchronously in the agent loop
  • BeforeAgent fires after user submits prompt, before planning
  • common uses include adding context, validating prompts, and blocking turns
  • packages/core/src/core/client.ts
  • fireBeforeAgentHookSafe() deduplicates once per prompt_id
  • the hook can stop, block, or return additionalContext
  • sendMessageStream() appends <hook_context>...</hook_context> to the request before processing the turn
  • packages/core/src/hooks/types.test.ts
  • additional context is sanitized by escaping < and </hook_context> delimiters

Freshness / trend signal

  • gh repo view openai/codex --json stargazerCount,forkCount,updatedAt
  • 66,427 stars, 8,868 forks, updated 2026-03-20T05:01:45Z
  • gh repo view google-gemini/gemini-cli --json stargazerCount,forkCount,updatedAt
  • 98,419 stars, 12,442 forks, updated 2026-03-20T04:45:10Z
  • Recent merged PRs suggest active runtime/governance work:
  • Codex: remote plugin sync, hooks-on-Windows disable, exec capture policy
  • Gemini CLI: admin-forced MCP installs, plan-mode clarification, ACP token usage metadata
  • gsio projects scope reports 100% analyzed coverage and fresh sync for both repos on 2026-03-20

Editorial guardrails

  • Do **not** claim both systems are identical
  • Do **not** claim hooks guarantee safety or correctness
  • Do **not** say Codex already has the same breadth of hook surface as Gemini
  • Emphasize **pre-turn middleware** / **ingress control**, not generic safety rhetoric

Ending question / CTA

If prompt submission becomes a programmable control surface, who should own that layer in practice: repo authors, platform teams, or end users building their own agent rituals?

Invite builders to inspect their own agent loops and publish where their real control plane lives.

Sources — 2026-03-20

Repo / trend checks

gh repo view openai/codex --json stargazerCount,forkCount,updatedAt,url


{"forkCount":8868,"stargazerCount":66427,"updatedAt":"2026-03-20T05:01:45Z","url":"https://github.com/openai/codex"}

gh repo view google-gemini/gemini-cli --json stargazerCount,forkCount,updatedAt,url


{"forkCount":12442,"stargazerCount":98419,"updatedAt":"2026-03-20T04:45:10Z","url":"https://github.com/google-gemini/gemini-cli"}

gsio --output json projects scope -p openai/codex -p google-gemini/gemini-cli


{
  "summary": {
    "project_count": 2,
    "commits_total": 10111,
    "commits_analyzed": 10111,
    "commits_missing": 0,
    "coverage": "100%",
    "stale_projects": 0,
    "stale_threshold_hours": 168
  },
  "projects": [
    {
      "project": "google-gemini/gemini-cli",
      "coverage": "100%",
      "analyzed": 5393,
      "missing": 0,
      "total": 5393,
      "run": "success",
      "synced_at": "2026-03-20T02:02:39.394Z",
      "stale": false
    },
    {
      "project": "openai/codex",
      "coverage": "100%",
      "analyzed": 4718,
      "missing": 0,
      "total": 4718,
      "run": "success",
      "synced_at": "2026-03-20T02:05:21.358Z",
      "stale": false
    }
  ]
}

Recent merged PRs


openai/codex
#15264 feat: Add One-Time Startup Remote Plugin Sync
#15263 fix: Distinguish missing and empty plugin products
#15254 core: add a full-buffer exec capture policy
#15253 Split features into codex-features crate
#15252 Disable hooks on windows for now

google-gemini/gemini-cli
#23163 feat(core): add support for admin-forced MCP server installations
#23158 fix(plan): clarify that plan mode policies are combined with normal mode
#23148 feat: ACP: Add token usage metadata to the `send` method's return value

Codex evidence

git show 6fef42165:codex-rs/hooks/src/events/user_prompt_submit.rs


19  pub struct UserPromptSubmitRequest {
20      pub session_id: ThreadId,
21      pub turn_id: String,
22      pub cwd: PathBuf,
23      pub transcript_path: Option<PathBuf>,
24      pub model: String,
25      pub permission_mode: String,
26      pub prompt: String,
27  }
...
59  pub(crate) async fn run(
64      let matched = dispatcher::select_handlers(
65          handlers,
66          HookEventName::UserPromptSubmit,
...
106     let should_stop = results.iter().any(|result| result.data.should_stop);
110     let additional_contexts = common::flatten_additional_contexts(
...
145             let trimmed_stdout = run_result.stdout.trim();
147             } else if let Some(parsed) =
148                 output_parser::parse_user_prompt_submit(&run_result.stdout)
...
156                 if parsed.invalid_block_reason.is_none()
157                     && let Some(additional_context) = parsed.additional_context
...
166                 if !parsed.universal.continue_processing {
...
182                 } else if parsed.should_block {
...
200                 let additional_context = trimmed_stdout.to_string();

git show 6fef42165:codex-rs/core/src/hook_runtime.rs


112 pub(crate) async fn run_user_prompt_submit_hooks(
117     let request = UserPromptSubmitRequest {
...
136 pub(crate) async fn inspect_pending_input(
141     let response_item = ResponseItem::from(pending_input_item);
142     if let Some(TurnItem::UserMessage(user_message)) = parse_turn_item(&response_item) {
143         let user_prompt_submit_outcome =
144             run_user_prompt_submit_hooks(sess, turn_context, user_message.message()).await;
145         if user_prompt_submit_outcome.should_stop {
146             PendingInputHookDisposition::Blocked {
147                 additional_contexts: user_prompt_submit_outcome.additional_contexts,
148             }
149         } else {
150             PendingInputHookDisposition::Accepted(Box::new(PendingInputRecord::UserMessage {
...
163 pub(crate) async fn record_pending_input(
174             sess.record_user_prompt_and_emit_turn_item(
180             record_additional_contexts(sess, turn_context, additional_contexts).await;
218 pub(crate) async fn record_additional_contexts(

git show 6fef42165:codex-rs/core/src/state/turn.rs


77  pub(crate) struct TurnState {
83      pending_input: Vec<ResponseInputItem>,
...
178 pub(crate) fn push_pending_input(&mut self, input: ResponseInputItem) {
179     self.pending_input.push(input);
182 pub(crate) fn prepend_pending_input(&mut self, mut input: Vec<ResponseInputItem>) {
187     input.append(&mut self.pending_input);
188     self.pending_input = input;
191 pub(crate) fn take_pending_input(&mut self) -> Vec<ResponseInputItem> {

Gemini evidence

docs/hooks/index.md


9  Hooks run synchronously as part of the agent loop—when a hook event fires,
10 Gemini CLI waits for all matching hooks to complete before continuing.
...
42 | `BeforeAgent` | After user submits prompt, before planning | Block Turn / Context | Add context, validate prompts, block turns |

packages/core/src/core/client.ts


142   private async fireBeforeAgentHookSafe(
167     const hookOutput = await this.config
168       .getHookSystem()
169       ?.fireBeforeAgentEvent(partToString(request));
172     if (hookOutput?.shouldStopExecution()) {
182     if (hookOutput?.isBlockingDecision()) {
192     const additionalContext = hookOutput?.getAdditionalContext();
193     if (additionalContext) {
194       return { additionalContext };
...
881   if (hooksEnabled && messageBus) {
882     const hookResult = await this.fireBeforeAgentHookSafe(request, prompt_id);
898     } else if ('additionalContext' in hookResult) {
904       { text: `<hook_context>${additionalContext}</hook_context>` },

packages/core/src/hooks/types.test.ts


183 it('getAdditionalContext should return additional context if present', () => {
190 it('getAdditionalContext should sanitize context by escaping <', () => {
193   additionalContext: 'context with <tag> and </hook_context>',
196   expect(output.getAdditionalContext()).toBe(
197     'context with &lt;tag&gt; and &lt;/hook_context&gt;',

LLM review

  • Model used: llm -m gpt-5.4
  • Verdict: pick the pre-turn hook interception angle because it is distinct from yesterday's concurrency story and easier to explain to a general technical audience.