Add a Codex project hook approval flow to Agent Mode - #678
Conversation
…ract Adopts the two methods the Agent Mode hook-approval flow (repoprompt#665) will call: hooks/list with its consumed per-cwd entry paths and an exhaustive trustStatus enum, and config/batchWrite with the upsert merge strategy pinned so upstream enum drift fails the gate instead of breaking trust writes at runtime. The generated ConfigEdit.value schema is unconstrained, so the fixture declares the closest projection and the schema-gate doc records that limitation plus post-write hooks/list as the semantic success authority for trust writes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fph5yrP8bV9yu3BjD2xh32
Typed hooks/list decoding (exhaustive trustStatus, UTF-8 byte identity for hook keys and hashes, fail-closed on unknown values, malformed records, and per-cwd discovery errors) and a CodexHookTrustService owning the list/preflight/write/verify trust transaction for repoprompt#665. The controller keeps two serialization boundaries: a per-controller operation mutex and an app-global trust-write mutex held until the config/batchWrite settles server-side, because concurrent app-server processes do read-modify-write with last-writer-wins on the shared config. Nothing calls the new operations yet; the Agent Mode gate arrives separately. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fph5yrP8bV9yu3BjD2xh32
Add a lazy per-controller-binding gate in CodexAgentModeCoordinator that discovers project PreToolUse hooks before the first startUserTurn on every dispatch route (direct send, fallback idle pump, terminal successor, managed-auth replay), suspends the turn behind a hook review request, and resumes only after the decision settles. Approvals write trusted_hash entries through the serialized trust service; inventory drift to zero resolves as a non-bypass external-resolution audit. Strict mode (approve-all-or-nothing) persists as a global setting with per-workspace overrides, resolved live at decision time; the app_settings MCP surface rejects writes to these human-only keys. Review requests carry a stable identity derived from tab, attempt, CWD, and sorted hook key/hash pairs so phase and error updates preserve the interaction while inventory changes replace it. Transcript notices for approval outcomes exclude hook keys, commands, paths, and hashes. Part of the repoprompt#665 hook-approval flow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fph5yrP8bV9yu3BjD2xh32
Add CodexHookReviewCard as the highest-precedence interaction card, with a review layout (per-hook metadata, selective approval, approve-all, continue) and a distinct discovery-failure layout (cwd error text, retry, continue). Selection is keyed by the stable interaction ID so phase and error updates preserve it while inventory drift resets it. The card observes global settings so strict mode hides continue-without-hooks live, and coordinator rejections surface visibly instead of being swallowed. Expose the same interaction headlessly: a hook_approval MCP interaction kind with phase-specific options, a hook_keys answers field for selective approval, an exact top-level argument allowlist, and atomic nested interaction/hook_gate snapshot decoding so wait/poll round trips never silently drop or normalize sections. The counts-only hook_gate audit includes resolved_externally, and a discovery-failure bypass reports its skipped count as unknown rather than zero. Phase capabilities (trust, bypass, retry, resolving) are centralized on the review phase type and drive the coordinator, UI, and MCP option projection from one matrix. Part of the repoprompt#665 hook-approval flow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fph5yrP8bV9yu3BjD2xh32
Replace the "Override for this workspace" checkbox with a three-state picker — App default (labeled with the live global value), Always require approval, Don't require approval — so the workspace scope of the setting is explicit instead of implied by an override flag. The global checkbox now reads and writes only the global value. Part of the repoprompt#665 hook-approval flow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fph5yrP8bV9yu3BjD2xh32
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4a7dcf0848
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Reject approve-selected in strict mode unless it covers every unresolved hook — a partial selection was completing the gate with the remainder skipped, contradicting the setting's contract. Approving all keys stays valid and non-strict partial approval is unchanged. After the post-write verification pass, treat any unexpected unresolved project hook in the verified inventory as drift: publish a refreshed review with a new interaction identity instead of completing the gate, so a hook added or modified during the trust write is never silently skipped. Hooks deliberately skipped by a non-strict partial approval are not drift. Addresses PR repoprompt#678 review feedback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fph5yrP8bV9yu3BjD2xh32
baron
left a comment
There was a problem hiding this comment.
Blocking on two correctness/availability issues in the new security gate:
-
[P1] Re-check strict mode after the approval write suspends
In
CodexAgentModeCoordinator.swift:943-1046, a partialapproveSelectedis checked against strict mode before awaitingtrustHooksForCurrentWorkspace. If strict mode is enabled while that request is suspended, the post-write path completes and memoizes the gate without re-reading the setting, leaving the intentionally skipped hooks unresolved.Before
completeCodexHookReview, re-read the effective strict-mode value. If it is enabled andverified.unresolvedProjectHooksis nonempty, refresh the review from the verified inventory rather than resuming the first turn. -
[P1] Add bounded recovery for an indefinitely unsettled trust write
CodexHookTrustService.swift:35-61,126-137deliberately sendsconfig/batchWritewithout a timeout, whileCodexNativeSessionController.swift:852-905holds both the controller-local mutex and the app-global trust-write mutex until that request settles. If the app server neither responds nor disconnects, one request blocks hook approval for every workspace indefinitely, and cancel/retry cannot recover.Preserve the no-late-write invariant, but add a settlement deadline that terminates the owning app-server process and awaits its exit before releasing serialization. Reconnect and use
hooks/listas semantic authority. Please also add fault-injection coverage and settlement/lock-wait diagnostics.
The final commit does correctly address the earlier strict partial-selection and post-write inventory-drift findings. All hosted checks are green at head 5f7daf2b2b88a0ccf6acb5e6a30b1790efab6fef, but the two paths above should be resolved before merge.
Audit disposition — liveness/security-administration blocker (2026-08-14)The approval model is strong in several respects, but Please add a bounded settlement deadline, generation fencing, and a recoverable poisoned-lane/retirement policy before merging. Discovery and parsing should also conservatively distinguish an actually empty hook inventory from an incomplete or malformed inventory. Then rerun the live trust/modified/bypass scenarios. |
Deep-review assessment — 2026-08-14Disposition: liveness/security blocker. The fail-closed first-turn gate, correlated inventory review, post-write verification, strict-mode policy, and headless parity are strong. The trust-write path holds both the controller-local mutex and the process-global trust-write mutex while issuing Use a bounded request and settlement deadline, generation-fence late completion, and define recovery for a timed-out/poisoned lane before releasing the locks. Also ensure malformed, oversized, or partially decoded |
Fixes #665.
What this does
Codex 0.145.0 only runs repo-level
.codexPreToolUse hooks when the effectiveCODEX_HOMEconfig carries a[hooks.state]trusted_hashapproval entry for each hook; otherwise the hooks are silently skipped. Because CE binds its bundled Codex app server to an app-privateCODEX_HOME, hook approvals users made in~/.codexnever carry over, so repo guardrail hooks (for example, hooks that blockterraform destroy) silently stopped running for CE Agent Mode sessions — fail-open, with no signal.This PR adds a fail-closed approval lifecycle. After a thread binds but before the first
turn/start, the coordinator lists project hooks for the execution cwd. Anyuntrustedormodifiedproject hook blocks the first turn behind a review interaction, surfaced both as a SwiftUI card and over theagent_runMCP contract. Approval writes thetrusted_hashentries throughconfig/batchWriteinto CE's private home, re-verifies with a secondhooks/list, and only then releases the pending turn. Live threads pick up the trust write without rebinding (verified against the pinned runtime).Design highlights
config/batchWriteis read-modify-write per process with no cross-process merge (verified empirically: concurrent writers clobber, last writer wins), so CE serializes all trust writes behind an app-global mutex, held until server-side settlement with cancellation shielding.hooks/listafter every write. Inventory drift to zero during review resolves as a distinctresolved_externallyaudit, never as a silent bypass.terraform destroy. The setting is human-only — theapp_settingsMCP surface rejects writes to it.hook_approvalinteraction with phase-specific options,hook_keysselective approval, an exact top-level argument allowlist, and atomic snapshot decoding so wait/poll round trips can't silently drop the interaction. Full hook metadata (source path, command, hash) is intentionally present in the pending interaction — it's required for informed approval — while diagnostics, stale-ID errors, transcripts, and audits stay counts-only.hooks/listandconfig/batchWriteshapes are pinned in the app-server contract fixture (floor 0.145.0) so runtime drift is caught bymake dev-codex-schema-check.Notes for reviewers
agent_runcaller (the user's orchestrator) resolves interactions. The gated session itself cannot self-approve — its first turn has not started while the gate is pending.commandon the matcher table) is silently ignored byhooks/listwith zero errors, indistinguishable from a hook-less repo. The gate cannot catch that class; candidate for an upstream codex report.FileSystemContentLoadingConcurrencyTests.testCancelledQueuedContentReadWorkerPermitWaitRecordsCancellationWithoutAcquisitionOrLeakfailed under full-suite load in two separate full-suite runs on this branch and passed focused (24/24) with zero content change both times; the subsystem is untouched here. Flagging as a pre-existing load-sensitive flake.Testing
dev-lint,dev-format-check, both product builds,dev-codex-schema-check, and the repopr-readypreflight lane green.trust_allover MCP wrote thehooks.stateentries and the deny hook then blockedbash -c trueon the same live thread with its stderr reason — no rebind. Changing the hook declaration re-triggered review asmodified;continue_without_hooksresumed the turn with the hook skipped and a counts-only transcript notice. On a real guarded infrastructure repo, both project hooks were trusted via the flow andterraform apply -helpwas then blocked by that repo's own guardrail gate whileechopassed through the same chain — the original Agent Mode silently skips untrusted Codex project hooks in isolated CODEX_HOME — security hooks fail open #665 bypass is closed with no over-blocking.Screenshots
Attached in the first comment: the settings card (default state, open picker, and global-on + workspace opt-out) and Agent Mode transcripts showing block-after-approval and the explicit-bypass notice.
Release note
Agent Mode may now pause before its first Codex turn to ask you to approve repository-declared Codex hooks (
.codexconfig). Approvals persist per hook version; an opt-in strict mode (Settings → Agent Mode) requires approval and removes the continue-without-hooks option.🤖 Generated with Claude Code
https://claude.ai/code/session_01Fph5yrP8bV9yu3BjD2xh32