Skip to content

fix(codex): honor live noninteractive permission hooks - #20860

Closed
andrew12-forge wants to merge 2 commits into
stablyai:mainfrom
andrew12-forge:fix/codex-noninteractive-permission-status
Closed

andrew12-forge wants to merge 2 commits into
stablyai:mainfrom
andrew12-forge:fix/codex-noninteractive-permission-status

Conversation

@andrew12-forge

@andrew12-forge andrew12-forge commented Sep 15, 2026 •

Copy link
Copy Markdown

ELI5

A Codex permission hook is not necessarily a question for a person. Orca currently marks even hooks whose live policy forbids human approvals as waiting, so a long command can produce a false “needs input” alert. This change honors the live hook's noninteractive mode and keeps real approvals and questions actionable.

What Changed

  • Normalize Codex PermissionRequest with permission_mode: "bypassPermissions" to working and omit the fabricated human approval card. Preserve request_user_input on both PreToolUse and PermissionRequest.
  • Carry optional, event-scoped codexNonInteractivePermission proof through the SSH relay so client reconciliation preserves the execution host's decision, including when a child genuinely needs input. The SSH session forwards the flag through its explicit envelope mapping. Do not persist that proof.
  • Preserve Codex question PreToolUse as waiting during remote lead-state reconciliation.

Why

Codex's hook documentation describes PermissionRequest as a pre-decision hook. PostToolUse can arrive only when a long unified-exec command finishes. In Codex 0.154.0's hook implementation, approval policy never maps to bypassPermissions; it is not proof that the sandbox is disabled. The approval implementation runs permission hooks before resolving approval.

Use that current per-hook policy instead of launch arguments, absent terminal prompts, transcript guesses, or a longer timeout. Missing, malformed, unknown, and human-review modes retain attention.

Linked Issue

Related to #13600; also relevant to #19618. Reuses the existing report rather than opening another issue.

This is a scoped fix for the noninteractive-hook variant. It does not close #13600: ordinary “Approve for me” routes OnRequest/Granular plus AutoReview through Codex's guardian reviewer and reports permission_mode: "default". That broader case remains unresolved by this PR. Existing PR #11046 proposes launch-argument ownership; #14715 defers synthetic bells. This change addresses live noninteractive status at the producer and leaves those broader proposals separate.

Visual Proof

N/A — no new rendered UI. Status and notification routing are covered by synthetic tests; no live desktop notification screenshot was captured.

Before/after regression: replay PreToolUse → noninteractive PermissionRequest → ten seconds without PostToolUse. With this classification disabled, the real notification coordinator emits an unwanted alert at 1.5 seconds and the test fails. With the fix, no attention or completion is dispatched during the ten seconds. An unanswered human approval and question still notify after 1.5 seconds.

Testing

All checks ran on macOS with ORCA_BACKGROUND_LAUNCH=1.

  • pnpm tc: passed.
  • pnpm lint: passed.
  • pnpm run check:code-quality:changed: passed, zero new findings.
  • Focused hook listener, host store, relay, and completion suites: 82 files / 869 tests passed.
  • Initial regression suites: 3 files / 30 tests passed, including authenticated local HTTP ingress, remote store normalization, folder workspaces, child waits, in-session policy changes, missing/invalid proof, and human questions.
  • Review follow-up adds five cases through the actual SSH session/multiplexer handler, covering noninteractive permissions, default/missing modes, and both question hook events. The new bypass-mode case fails before the forwarding fix (waiting instead of working). All four follow-up suites passed: 45 tests. Full typecheck and the changed-code quality gate also passed after the fix.
  • pnpm run build:relay and pnpm run build:electron-vite: passed. These are build checks, not live Windows/Linux/SSH runs; host-only dependencies were installed.
  • Full test suite and native packaging were not run. No installed app restart, settings change, or visible test window was needed.

Reproduce the targeted tests with ORCA_BACKGROUND_LAUNCH=1 pnpm test src/shared/agent-hook-listener-codex-permissions.test.ts src/main/agent-hooks/server-codex-noninteractive-permissions.test.ts src/renderer/src/components/terminal-pane/agent-completion-codex-live-permissions.test.ts.

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

AI Disclosure

Implemented and reviewed with OpenAI Codex.

Review

Pullfrog found that the initial SSH tests bypassed a field-by-field session mapping which dropped the new flag. The follow-up fixes that missed boundary and adds transport-level regression coverage. It also removes wording that conflated noninteractive hooks with ordinary “Approve for me.”

  • Security: only status presentation changes; no approval decision or permission is granted. Local proof comes from the current hook, not an outer caller-supplied flag. Remote proof is restricted to Codex permission events and cannot override a named human question.
  • Platforms and integrations: shared TypeScript with no platform, filesystem, shell, or Git-provider assumptions. Other agents retain their existing behavior.
  • SSH/local/mobile: execution-host normalization feeds the existing authoritative store and downstream readers. Folder workspaces need no Git metadata.
  • Wire compatibility: optional JSON metadata, no opcode or required-field change. Old/missing proof retains prior attention behavior; old clients can still show the old false positive. New peers preserve the noninteractive parent while keeping a child's human wait visible.
  • Performance/UI: constant-time field checks, no new timers, polling, transcript reads, or rendered components.

Agent skill upstream boundary

  • Not applicable; no upstream skill-installer code or artifacts were copied.

Notes

All new reproduction data is synthetic. Raw session evidence, screenshots, local paths, and unrelated workspace content are omitted.

Checklist

  • This PR is small and focused
  • I explained what changed and why (including ELI5)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred) — exact local coverage and limits are listed above

@coderabbitai

coderabbitai Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: e55abb74-5002-4968-8c77-70f9ba072f50

📥 Commits

Reviewing files that changed from the base of the PR and between aac81f4 and e1ac6af.

📒 Files selected for processing (6)
  • src/main/agent-hooks/server-codex-noninteractive-permissions.test.ts
  • src/main/ssh/ssh-relay-session-agent-hooks.integration.test.ts
  • src/main/ssh/ssh-relay-session.ts
  • src/renderer/src/components/terminal-pane/agent-completion-codex-live-permissions.test.ts
  • src/shared/agent-hook-listener-codex-permissions.test.ts
  • src/shared/agent-hook-listener/providers/codex-permission-request.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/shared/agent-hook-listener-codex-permissions.test.ts
  • src/shared/agent-hook-listener/providers/codex-permission-request.ts
  • src/main/agent-hooks/server-codex-noninteractive-permissions.test.ts
  • src/renderer/src/components/terminal-pane/agent-completion-codex-live-permissions.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.


📝 Walkthrough

Walkthrough

Codex permission requests with bypassPermissions are classified as non-interactive unless they use an ask-user-question tool. These requests enter working without an interactive prompt. Other permission requests enter waiting. The non-interactive flag is normalized, propagated through relay and remote ingestion, used during state reconciliation, and excluded from persisted status data. Tests cover local hooks, SSH normalization, proof validation, human prompts, and notification timing.

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to e1ac6

The updated permission flow preserves human prompts while suppressing attention for verified automatic permissions. No current merge-blocking risk is identified.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The changes satisfy the live Codex hook variant of issue #13600. PermissionRequest events with permission_mode: 'bypassPermissions' become working without an approval prompt. Normal approvals an… Implement detection and propagation for the actual “Approve for me” auto-review outcome, or an equivalent authoritative noninteractive state. Suppress attention notifications while that state handles the request. Add a regression test that …
Docstring Coverage ⚠️ Warning Docstring coverage is 21.43% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 14 functions across 17 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed The changed state handling, SSH relay propagation, persistence exclusion, notification routing, and regression tests support issue #13600. They distinguish noninteractive Codex permission hooks from h…
Title check ✅ Passed The title clearly and concisely describes the main change: honoring live noninteractive Codex permission hooks.
Description check ✅ Passed The description is detailed and covers the required explanation, changes, rationale, linked issues, visual proof, testing, AI disclosure, review considerations, compatibility, and checklist items. It …
Full details: Linked Issues check

Explanation

The changes satisfy the live Codex hook variant of issue #13600. PermissionRequest events with permission_mode: 'bypassPermissions' become working without an approval prompt. Normal approvals and question tools remain waiting. The relay carries this event-scoped proof and tests cover local and remote handling. However, the linked issue also requires suppression when the “Approve for me” auto-review flow handles a request. The PR summary states that this case remains unresolved, and the reviewed implementation only detects the live bypassPermissions hook condition.

Resolution

Implement detection and propagation for the actual “Approve for me” auto-review outcome, or an equivalent authoritative noninteractive state. Suppress attention notifications while that state handles the request. Add a regression test that covers the issue’s auto-review scenario and confirms that genuine human waits still notify.

  • Fix all pre-merge checks with AI

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

The SSH relay path drops the new codexNonInteractivePermission proof before ingestRemote, so on SSH a Codex noninteractive PermissionRequest still lands as waiting — the exact false "needs input" this PR claims to fix. Local and WSL behave correctly.

Reviewed changes

  • Codex permission classifier — new isCodexNonInteractivePermissionRequest treats PermissionRequest + permission_mode: "bypassPermissions" + a non-question tool as host-side proof the hook cannot ask a human.
  • Host normalization — codex-events/codex-tool-fields map that case to working with no approval card; codex-state adds a PreToolUse + ask-user → waiting branch and honors the proof in remote reconciliation.
  • Relay wire + main ingest — optional codexNonInteractivePermission on the envelope, re-validated in server-ingest-remote, consumed by reconcileRemoteCodexState, and stripped before persistence.
  • Tests — listener, server-ingest, and renderer attention suites; all synthetic.

🚨 SSH ingress drops the proof before ingestRemote

Unlike the WSL link (which forwards the whole JSON‑RPC params object), the SSH session reconstructs the envelope field‑by‑field and omits the new flag. ingestRemote reads it from that reconstructed object, so on SSH it is always undefined; reconcileRemoteCodexState then re-derives waiting. This leaves the primary remote scenario advertised as blocked with no answerable card (the host already cleared interactivePrompt). The new tests call server.ingestRemote directly, so they never exercise this boundary and stay green.

Technical details
# SSH relay discards `codexNonInteractivePermission`

## Affected sites
- `src/main/ssh/ssh-relay-session.ts:1575-1610` — the explicit remap into `agentHookServer.ingestRemote` enumerates `grokPromptBoundary`, `compactTrigger`, `toolUseId`, `shedFields`, etc., but never `codexNonInteractivePermission`. Add it beside `compactTrigger` (line 1593).
- `src/main/agent-hooks/server/server-ingest-remote.ts:266-272` — the sole consumer reads `envelope.codexNonInteractivePermission`; with the field absent it produces `undefined`.
- `src/main/agent-hooks/server/server-status-update.ts:86-94` → `reconcileRemoteCodexState` → `codexLeadStateForHookEvent('PermissionRequest', false)` returns `waiting`.
- `src/main/ssh/ssh-relay-session-agent-hooks.integration.test.ts:580-637` — the "forwards remote hook transition metadata" test enumerates fields and would not catch the drop; `server-codex-noninteractive-permissions.test.ts` bypasses the mapping via direct `ingestRemote`.
- Reference: the WSL path is unaffected because `src/main/agent-hooks/wsl-hook-relay-link.ts:43` forwards `params` wholesale.

## Required outcome
- On SSH, a Codex `PermissionRequest` with `permission_mode: "bypassPermissions"` and a non-`request_user_input` tool reaches `ingestRemote` with the proof, reconciles to `working`, and dispatches no attention.
- A `default`-mode Codex approval and any `request_user_input` question still reconcile to `waiting`/attention on SSH.
- An SSH-boundary assertion covers the new field so the next wire field cannot be silently dropped.

## Suggested approach
Add `codexNonInteractivePermission: envelope.codexNonInteractivePermission === true ? true : undefined` to the literal in `wireUpAgentHookEvents`, and extend the SSH agent-hooks integration test's `objectContaining` with the flag for a codex `PermissionRequest` envelope.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

eventName: unknown,
hookPayload: Record<string, unknown>
): boolean {
// Codex maps approval_policy=never to bypassPermissions, including sandbox auto-review.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "including sandbox auto-review" claim does not hold against the same Codex source: guardian auto-review routes only when approval_policy ∈ {OnRequest, Granular} with approvals_reviewer == AutoReview, which maps to "default", not "bypassPermissions". Those auto-review requests therefore still surface waiting. The PR body scopes this out explicitly, so the comment is the only misleading part — consider dropping the auto-review clause.

@natasha-poetic

Copy link
Copy Markdown

+1

@brennanb2025

Copy link
Copy Markdown
Contributor

Superseded by #21389, which is merged.

You identified the right defect — a Codex permission hook is not necessarily a question for a person — and the request_user_input carve-out you preserved was exactly right; the shipped fix keeps it for the same reason.

One thing worth recording, because we only learned it by measuring: permission_mode alone cannot separate the two cases. We captured a real phantom from Codex 0.155.0 and the payload carried permission_mode: "default". "Approve for me" is a separate reviewer axis (approvals_reviewer), not an approval policy, and only Full Access resolves to bypassPermissions. So this approach would have fixed the bypass-flag case while leaving the reported one intact. The merged change reads the reviewer from the turn's rollout instead.

Thanks for the work, and for spotting that the event is pre-decision.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Codex “Approve for me” can still trigger false attention notifications after #8519

3 participants