Before the Prompt Lands: Codex and Gemini Turn Hooks Into Agent Control Planes
March 20, 2026 / Daily Edition / 2 source signals.
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
6fef42165addsUserPromptSubmithooks. 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.rsinspect_pending_input()runsUserPromptSubmitbefore 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
BeforeAgentfires after user submits prompt, before planning- common uses include adding context, validating prompts, and blocking turns
packages/core/src/core/client.tsfireBeforeAgentHookSafe()deduplicates once perprompt_id- the hook can stop, block, or return
additionalContext sendMessageStream()appends<hook_context>...</hook_context>to the request before processing the turnpackages/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 scopereports 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 <tag> and </hook_context>',
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.