Skip to content

Keep Codex permission hooks non-blocking - #5507

Merged
lawrencecchen merged 34 commits into
mainfrom
feat-codex-permission-hook
Jun 8, 2026
Merged

lawrencecchen merged 34 commits into
mainfrom
feat-codex-permission-hook

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Treat Codex PermissionRequest Feed hooks as non-blocking telemetry so Codex Approve for me can run its own reviewer.
  • Install Codex hook timeouts in seconds (5) instead of accidentally writing millisecond values into Codex config.
  • Clean stale Codex hook trust hashes for the bad timeout values and document the Codex approval boundary.

Verification

  • git diff --check origin/main..HEAD
  • CMUX_CLI_BIN="/Users/lawrence/Library/Developer/Xcode/DerivedData/cmux-codexp/Build/Products/Debug/cmux DEV codexp.app/Contents/Resources/bin/cmux" python3 tests/test_codex_feed_hooks.py
  • ./scripts/reload-cloud.sh --tag codexp

Dogfood

Use the tagged build and run Codex with hooks installed in Approve for me. Trigger a command that would have previously shown a Feed PermissionRequest. The expected behavior is that Codex continues through its own approval or auto-review path without waiting for a cmux Feed approval.


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Note

High Risk
Changes permission/approval routing for Codex across hooks, app-server JSON-RPC, and Feed UI; incorrect bridging could block workflows or mis-map security-sensitive decisions.

Overview
Codex permission hooks no longer block on Feed: PermissionRequest is classified as telemetry (like PreToolUse), feed hooks use zero wait and 5s installed timeouts (seconds, not mistaken millisecond values), so Codex “Approve for me” can run before cmux intercepts.

Real Codex approvals move to cmux codex-teams: a new CodexTeamsApprovalBridge and watcher intercept app-server requestApproval RPCs, push actionable cards via feed.push (~120s), map Feed decisions back to Codex (including availableDecisions / policy amendments), and fall through to native Codex when Feed fails or does not resolve—without auto-denying.

Feed UI and notifications now gate permission actions per request (once / always / all / bypass) using tool_input_capabilities, including dynamic macOS notification categories and an “All tools” action. codex-teams validates -C/--cwd directories and authenticates isolated socket clients for the watcher.

Reviewed by Cursor Bugbot for commit bb19d16. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Keep Codex PermissionRequest/PreToolUse hooks non-blocking telemetry and route real approvals through the cmux codex-teams bridge so Codex “Approve for me” runs first. Gate once/always/all/bypass per request, harden sockets/UI/cwd checks, and fall back to Codex’s native reviewer when Feed can’t resolve or times out.

  • New Features

    • cmux codex-teams bridges item/commandExecution|fileChange|permissions/requestApproval to Feed via feed.push, isolates ~120s waits, ignores unsupported app-server requests, validates -C/--cd/--cwd, authenticates and closes sockets/watchers, and caches up to 500 related items.
    • Maps user decisions back to Codex honoring availableDecisions and policy amendments; gates once/always/all/bypass per request based on Codex-advertised capabilities (tool_input_capabilities); keeps scopes explicit; file-change approvals are one‑shot by default; preserves persistent approvals when supported.
    • Feed panel, TUI, and notifications only show allowed actions with dynamic categories (including “All tools”); Enter respects availability.
  • Bug Fixes

    • Reclassify Codex PermissionRequest as telemetry (like PreToolUse); use 5s hook timeouts (seconds), clean bad trust hashes, and never auto‑deny on bridge timeouts.
    • Return JSON‑RPC errors for malformed app-server approval requests; avoid denial on unsupported persistent replies; on Feed failure/timeout, return a no‑decision native fallback so Codex handles approval; docs/tests updated (Swift Testing, serialized) for cwd validation, capability gating, and decision mapping.

Written for commit bb19d16. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes / Improvements

    • Codex permission requests are now treated as non-blocking telemetry, preserving native approval/auto-approve flows.
    • Improved CLI working-directory resolution and per-event feed/bridge timeout handling.
    • Persistent-permission mode handling adjusted so Codex behaves consistently with non-blocking flows.
  • Documentation

    • Clarified agent-hook, feed timeout, and decision semantics for Codex feed/approval behavior.
  • Tests

    • Expanded tests for telemetry-only permission requests, timeout expectations, decision mapping, and CLI cwd resolution.

@vercel

vercel Bot commented Jun 6, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 8, 2026 8:10am
cmux-staging Building Building Preview, Comment Jun 8, 2026 8:10am

@coderabbitai

coderabbitai Bot commented Jun 6, 2026 •

Copy link
Copy Markdown

Ready to act? Review this PR in Change Stack to turn feedback into patch suggestions you can inspect and refine.

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Codex PermissionRequest shifts to non-blocking telemetry (PreToolUse/.toolStart). cmux bridges Codex app-server approval notifications into Feed PermissionRequest events, normalizes feed decisions into app-server responses, tightens per-agent hook timeouts (Codex: 5s), adds working-directory CLI resolution/validation, and updates Swift/Python tests and docs.

Changes

Codex PermissionRequest as Non-blocking Telemetry

Layer / File(s) Summary
Feed event classification and tests
CLI/FeedEventClassifier.swift, cmuxTests/FeedEventClassificationTests.swift, CLI/CMUXCLI+AgentHookDefinitions.swift, cmuxTests/CLIGenericHookPersistenceTests.swift, tests/test_codex_feed_hooks.py
Codex PermissionRequest is classified as telemetry (.toolStart/PreToolUse); tests and hook-install assertions updated to expect non-actionable telemetry and short Codex hook timeouts.
Permission policy predicate tweak
Sources/Feed/FeedPermissionActionPolicy.swift, cmuxTests/FeedCoordinatorTests.swift
supportsPersistentPermissionModes no longer special-cases .codex; tests updated to reflect the changed predicate.
Codex Teams approval bridge & watcher
CLI/cmux.swift, cmuxTests/FeedCoordinatorTests.swift
Centralize app-server message handling, cache approval items, forward approval requests into Feed PermissionRequest, derive normalized permission mode from push responses, map modes into method-specific app-server responses, and forward caller timeouts on respond; coordinator tests validate mappings and timed-out behavior.
Working-directory CLI resolution & validation
CLI/cmux.swift, cmuxTests/FeedCoordinatorTests.swift
Add codexTeamsResolvedWorkingDirectory parsing -C/--cd/--cwd/--cd= with ~ expansion and baseDirectory resolution; validateCodexTeamsWorkingDirectory enforces existence and directory-ness with tests for flag ordering and missing dirs.
Feed hook timeout policy and normalization
CLI/cmux.swift
Introduce per-event feedHookTimeoutMs(for:) (Codex: short timeout), restrict nested ms→seconds normalization to codex and grok, and compute per-event feed timeouts for bridge entries.
Generated hook entries and persistence tests
CLI/cmux.swift, cmuxTests/CLIGenericHookPersistenceTests.swift
Regenerate codex-hook and feed-hook command entries with updated command helpers and timeout arrays ([5_000, 600] for codex-hook, [120_000, 120, 600] for feed-hook); tests assert timeout == 5 for Codex hooks.
Python integration tests: non-blocking telemetry
tests/test_codex_feed_hooks.py
Replace permission-reply test with test_codex_permission_request_is_nonblocking_telemetry, assert no decision stdout, zero wait timeout, PreToolUse telemetry _source == "codex", include persistent-mode (deny) coverage emitting no Codex decisions, and update main() message.
Documentation alignment
docs/agent-hooks.md, docs/feed.md
Document Codex feed bridge as PreToolUse, PermissionRequest telemetry; explain Codex keeps approval prompts in its app-server UI while cmux records non-blocking telemetry; clarify timeout semantics for blocking bridge vs. Codex feed hooks.

Sequence Diagram(s)

sequenceDiagram
  participant CodexAppServer
  participant CmuxWatcher
  participant Feed
  participant AppServerConnection
  CodexAppServer->>CmuxWatcher: item/*/requestApproval notification
  CmuxWatcher->>Feed: feed.push PermissionRequest event
  Feed-->>CmuxWatcher: push response (mode / timed_out)
  CmuxWatcher->>AppServerConnection: respond(mapped decision, timeout)
  AppServerConnection-->>CodexAppServer: approval response payload
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • manaflow-ai/cmux#5320: Related changes to agent-hook Feed telemetry non-blocking classification and push semantics.
  • manaflow-ai/cmux#5010: Related FeedEventClassifier changes for actionable vs telemetry event classification.
  • manaflow-ai/cmux#4225: Related nested/feed hook timeout normalization adjustments for codex/grok.

Poem

🐰 I nibbled through the hook and spun,

Codex requests now let the app-server run.
Telemetry hops, five seconds light,
No blocking pause, the flow stays bright.
Hooray — small hops, big delight!


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (8 errors, 1 warning, 2 inconclusive)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error PR introduces reconcileWaiter DispatchSemaphore blocking-wait timeout used as timer instead of real signal; lacks required cancellation-aware scheduler or timer abstraction. Replace reconcileWaiter semaphore with Timer/DispatchSourceTimer; document why actor-based synchronization unsuitable for state locks.
Cmux No Hacky Sleeps ❌ Error PR introduces scripts/download-with-retry.sh with exponential backoff retry using wall-clock sleep(), violating rule: retry logic must not depend on elapsed wall-clock time. Replace sleep-based retry in scripts/download-with-retry.sh with event-driven or callback-based approach, or implement dedicated cancellation-aware retry abstraction with proper timeout semantics.
Cmux Swift File And Package Boundaries ❌ Error CLI/cmux.swift violates boundaries: adds 490 net lines to oversized file (32,615 lines, 57% over 20,530 budget). Mixes networking, state, parsing, and Feed integration without package extraction. Extract CodexTeams into new CmuxCodexAppServer SwiftPM package: move CodexTeamsAppServerConnection, CodexTeamsWatcher, and approval-decision mapping. Move work-dir validation to utility package.
Cmux Swift Logging ❌ Error PR adds multiple fputs() stderr logging calls in production code (e.g., line 18423) and error messages exposing raw method names, violating swift-logging.md rules. Replace fputs() with os.log Logger. Sanitize error messages to neutral text like "Unsupported approval request" instead of exposing method names.
Cmux User-Facing Error Privacy ❌ Error Line 18410 in CLI/cmux.swift exposes internal provider-specific method names via message: "\\(method) is missing params", violating user-facing error privacy policy. Replace with generic message like "Malformed approval request" instead of interpolating provider-specific method name.
Cmux Full Internationalization ❌ Error CLI/cmux.swift adds two non-localized user-facing error messages violating Swift error text localization rule. Use String(localized:defaultValue:) for error messages at lines 18296 and 18412, add to Localizable.xcstrings for en and ja.
Cmux Architecture Rethink ❌ Error Introduces NSLock-guarded mutable state (subscribedThreadIds, approvalItemById) in CodexTeamsWatcher creating duplicate ownership of subscription/caching state already managed elsewhere. Eliminate manual locks by making one explicit actor/task own subscription state. Consolidate caching into a single coordinator with clear invariants rather than split lock-protected collections.
Cmux Source Artifacts ❌ Error PR adds 8 tool/IDE artifacts violating source-control-artifacts.md: .claude/scheduled_tasks.lock (session state), 6 .claude/commands files, and .cursor/rules/main.mdc (explicitly listed as failing). Remove all .claude/ files (7 total) and .cursor/rules/main.mdc; optionally add to .gitignore.
Docstring Coverage ⚠️ Warning Docstring coverage is 3.03% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Cmux Algorithmic Complexity ❓ Inconclusive No result was produced after verification. Marking as INCONCLUSIVE. Re-run the check or adjust instructions to produce a final result.
Cmux Swift Concurrency ❓ Inconclusive No result was produced after verification. Marking as INCONCLUSIVE. Re-run the check or adjust instructions to produce a final result.
✅ Passed checks (8 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the primary change: treating Codex permission hooks as non-blocking telemetry to preserve Codex's own approval flow.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed New classes use synchronous local patterns. CodexTeamsAsyncBox properly marked @unchecked Sendable with NSLock. No implicit MainActor violations or cross-isolation boundary access introduced.
Cmux Swift @Concurrent ✅ Passed No Swift concurrency violations found. All async work (network I/O, readiness probes) properly uses DispatchQueue.global to hop off the caller context. No missing or invalid @concurrent annotations.
Cmux Swiftui State Layout ✅ Passed PR contains no SwiftUI changes. All modifications are CLI code, non-SwiftUI logic, tests, and documentation with no new observable state, state mutations, or layout issues introduced.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed No user-visible NSWindow, NSPanel, NSWindowController, SwiftUI Window, or WindowGroup code added or materially changed. PR modifies only logic, data handling, tests, and documentation.
Description check ✅ Passed PR description exceeds template requirements with comprehensive summary, verification steps, and dogfooding details; all required sections covered.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-codex-permission-hook

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 and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 970950f280

ℹ️ 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".

"Codex setup should collapse duplicate cmux-owned prompt hooks to one entry, saw \(allCommands)"
)
XCTAssertTrue(
codexHookEntries.contains {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Declare codexHookEntries before asserting timeouts

When cmuxTests is built, this new assertion references codexHookEntries, but this test only defines allCommands above and never binds the flattened hook dictionaries. The symbol exists in a different test method, so the suite will fail to compile before any Codex hook regression tests can run.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmuxTests/CLIGenericHookPersistenceTests.swift`:
- Around line 3143-3156: The test
testCodexHookInstallPrefersLaunchingAppBundledCLI references codexHookEntries
which is undefined; declare and initialize codexHookEntries before the
XCTAssertTrue calls by filtering the existing hookEntries collection (e.g., let
codexHookEntries = hookEntries.filter { ($0["source"] as? String) == "codex" }
or an equivalent predicate that selects Codex hooks) so the subsequent
assertions can compile and validate the Codex hook entries. Ensure you reference
the same hookEntries variable used earlier in the test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1c4444d6-ecb2-4b6c-8d7d-896521d08803

📥 Commits

Reviewing files that changed from the base of the PR and between 83dfeab and 970950f.

📒 Files selected for processing (8)
  • CLI/CMUXCLI+AgentHookDefinitions.swift
  • CLI/FeedEventClassifier.swift
  • CLI/cmux.swift
  • cmuxTests/CLIGenericHookPersistenceTests.swift
  • cmuxTests/FeedEventClassificationTests.swift
  • docs/agent-hooks.md
  • docs/feed.md
  • tests/test_codex_feed_hooks.py

Comment thread cmuxTests/CLIGenericHookPersistenceTests.swift Outdated
@greptile-apps

greptile-apps Bot commented Jun 6, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR makes Codex PermissionRequest hook events non-blocking telemetry, fixes the Codex hook timeout from an accidental millisecond value to 5 seconds, and introduces CodexTeamsApprovalBridge — a new bridge that routes real Codex app-server approval requests to Feed and maps user decisions back.

  • FeedEventClassifier: Codex PermissionRequest is reclassified from approvalRequest to toolStart so hook-level events no longer block Codex's own "Approve for me" path; cmux codex-teams now owns actual approval bridging via a dedicated watcher that suppresses previously-unresolved approvals.
  • Dynamic permission capabilities: Feed panel, TUI, and macOS notification categories now gate once/always/all/bypass buttons per Codex-advertised availableDecisions; the capability-parsing logic is independently implemented in three layers.
  • Fallback safety: When Feed times out or fails, the watcher suppresses the approval key without responding so Codex's native TUI can answer; suppression keys survive watcher reconnects, which can silently consume re-presented approvals on the fresh connection.

Confidence Score: 3/5

The core reclassification and timeout fix are safe; the watcher suppression logic has a reconnect gap that can leave Codex app-server requests permanently unanswered on a live connection.

The approval bridge suppression mechanism does not reset on watcher reconnect. If Codex re-presents a previously-timed-out approval on the fresh connection, the watcher silently consumes the request without sending any JSON-RPC response, leaving it stalled with no path to Codex's native TUI.

CLI/cmux.swift — the handleApprovalRequest suppression path and resetConnectionSubscriptions, which should also clear suppressedApprovalKeys to allow re-presented approvals to be forwarded on a fresh connection.

Important Files Changed

Filename Overview
CLI/CodexTeamsApprovalBridge.swift New 539-line file encapsulating Codex app-server to Feed approval translation; decision-mapping logic is well-tested but codexCapabilities parsing is duplicated in two other layers.
CLI/cmux.swift Large additions to the codex-teams watcher: thread subscriptions, approval caching, Feed bridging, suppression logic. Suppression keys are not cleared on reconnect, which can silently drop re-presented Codex approval requests on a live connection.
CLI/FeedEventClassifier.swift Reclassifies Codex PermissionRequest from approvalRequest to toolStart (non-blocking telemetry); intentional and matches the new approval bridge architecture.
CLI/CMUXCLI+AgentHookDefinitions.swift Fixes Codex hook timeout from 5000 (mistakenly written as milliseconds) to 5 (seconds); adds stale trust-hash cleanup for old bad values.
Sources/AppDelegate.swift Generates dynamic UNNotificationCategory variants and gates notification action delivery on item capabilities.
Sources/Feed/FeedPermissionActionPolicy.swift Adds per-request capability checks and codexCapabilityToolInputJSON extraction; logic mirrors CodexTeamsApprovalBridge in the app layer with WorkstreamSource types.
Sources/Feed/FeedCoordinator.swift Computes dynamic notification category ID from event capabilities and emits tool_input_capabilities as a compact field to preserve capability info when tool_input is truncated.
Resources/feed-tui/index.ts TypeScript TUI gains capability-gated permission actions and blocks Enter when once is unsupported; codexCapabilities logic is an independent reimplementation of the Swift versions.

Sequence Diagram

sequenceDiagram
    participant Codex as Codex App-Server
    participant Watcher as cmux codex-teams watcher
    participant Feed as cmux Feed socket
    participant UI as Feed UI

    Codex->>Watcher: item/commandExecution/requestApproval
    Watcher->>Watcher: check suppressed?

    alt Not suppressed
        Watcher->>Feed: feed.push wait 120s
        Feed->>UI: PermissionRequest card
        UI-->>Feed: User decision
        Feed-->>Watcher: resolved mode always
        Watcher->>Codex: decision acceptForSession
    else Feed timed out
        Watcher->>Watcher: suppressApproval
        Note over Watcher,Codex: No response sent
    else Suppressed on reconnect
        Watcher->>Watcher: return consumed
        Note over Watcher,Codex: Request stalls on live connection
    end
Loading

Reviews (15): Last reviewed commit: "Keep Codex approval fallback native" | Re-trigger Greptile

Comment on lines +3143 to +3155
XCTAssertTrue(
codexHookEntries.contains {
($0["command"] as? String)?.contains("hooks codex prompt-submit") == true
&& ($0["timeout"] as? Int) == 5
},
"Codex lifecycle hooks must use Codex's second-based timeout field, saw \(codexHookEntries)"
)
XCTAssertTrue(
codexHookEntries.contains {
($0["command"] as? String)?.contains("hooks feed --source codex --event PermissionRequest") == true
&& ($0["timeout"] as? Int) == 5
},
"Codex Feed hooks are telemetry and must use a short second-based timeout, saw \(codexHookEntries)"

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.

P0 codexHookEntries is out of scope — compile error

codexHookEntries is only defined inside testGrokHookInstallPinsInstallingCLIAndSocketWithoutCMUXInterpolation (line 2918), a completely different function that reads from .grok/hooks/cmux-session.json. The two new assertions here are inside testCodexHookInstallPrefersLaunchingAppBundledCLI, which still derives only allCommands at line 3116-3121 without ever binding codexHookEntries. The fix is to split the allCommands derivation in testCodexHookInstallPrefersLaunchingAppBundledCLI the same way the Grok function was split — introduce a codexHookEntries binding there and derive allCommands from it.

Comment on lines +2918 to +2923
let codexHookEntries = hooks.values
.compactMap { $0 as? [[String: Any]] }
.flatMap { $0 }
.compactMap { $0["hooks"] as? [[String: Any]] }
.flatMap { $0 }
let allCommands = codexHookEntries

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.

P2 Misleading variable name in a Grok test

The variable is named codexHookEntries but this function (testGrokHookInstallPinsInstallingCLIAndSocketWithoutCMUXInterpolation) reads from .grok/hooks/cmux-session.json. The rename was clearly intended for the Codex test function, and this Grok function only needs it as a stepping stone to allCommands — a neutral name like hookEntries would be more accurate here.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Implemented the Codex app-server approval bridge on top of the non-blocking hook fix.

Verification:

  • Built tagged app via ./scripts/reload-cloud.sh --tag codxp5.
  • Ran CMUX_CLI_BIN=.../cmux DEV codxp5.app/Contents/Resources/bin/cmux python3 tests/test_codex_feed_hooks.py, passing with PASS: Codex Feed hooks leave Codex approvals non-blocking.
  • Dogfooded cmux codex-teams in the tagged app with a command approval. Feed showed a Codex permissionRequest; replying through feed.permission.reply with mode=once made Codex proceed and create /tmp/cmux-codex-feed-dogfood-work5/feed-button.txt containing codex-feed-dogfood.

Research result from openai/codex: PermissionRequest hooks run before Codex guardian/auto-review, and there is no post-auto-review hook before user approval. This PR keeps that hook telemetry-only and bridges the real app-server command/file approval requests instead.

Comment thread CLI/cmux.swift

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dc3f0c9963

ℹ️ 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".

Comment thread CLI/cmux.swift
private let readinessLock = NSLock()
private let stateLock = NSLock()
private var lastAgentSurfaceId: String?
private var subscribedThreadIds = Set<String>()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reset thread subscriptions per app-server connection

This set is kept on the watcher across the outer reconnect loop, but thread/resume subscriptions are tied to the current WebSocket connection. After a transient app-server disconnect, backfillLoadedThreads will call subscribeToThreadIfNeeded on the new connection, see the old thread ids already present, and skip thread/resume, so existing Codex threads stop delivering approval requests/thread updates until the watcher process is restarted.

Useful? React with 👍 / 👎.

Comment thread CLI/cmux.swift Outdated
?? requestIdString(requestId)
let cwd = stringValue(in: params, keys: ["cwd"])
let reason = stringValue(in: params, keys: ["reason"])
let command = stringValue(in: params, keys: ["command"])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve argv-array commands in Codex approvals

Codex app-server command approval payloads commonly carry command as an argv array rather than a string; in that case stringValue returns nil and the forwarded Feed item omits both tool_input.command and context.toolSummary. For normal shell approvals the Feed card then cannot show the command the user is being asked to approve, making the new app-server approval path unsafe/confusing even though a command was supplied.

Useful? React with 👍 / 👎.

Comment thread CLI/cmux.swift Outdated
guard method == "item/commandExecution/requestApproval"
|| method == "item/fileChange/requestApproval"
else { return false }
guard let params = message["params"] as? [String: Any] else { return true }

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.

P1 Missing JSON-RPC response for malformed approval request — when params is absent or not a dictionary, the function returns true (consumed) but never calls connection.respond(...). The Codex app-server issued a JSON-RPC request with a requestId and will block waiting for a reply that never arrives, which stalls that Codex thread indefinitely.

Suggested change
guard let params = message["params"] as? [String: Any] else { return true }
guard let params = message["params"] as? [String: Any] else {
// Malformed request: still ack with an empty result so the
// app-server doesn't block waiting on a reply that never arrives.
try? connection.respond(requestId: requestId, result: [:])
return true
}

Comment thread CLI/cmux.swift
Comment on lines +18351 to 18356
private func pushCodexApprovalToFeed(event: [String: Any]) throws -> [String: Any] {
try socketClient.sendV2(method: "feed.push", params: [
"event": event,
"wait_timeout_seconds": 120
], responseTimeout: 125)
}

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.

P1 Approval wait blocks the entire notification loop — pushCodexApprovalToFeed calls socketClient.sendV2(..., responseTimeout: 125), a synchronous 125-second wait on the same thread that runs listenForNotifications. While one approval is in flight, connection.receiveObject() never returns, so thread-state updates, new approval requests from parallel Codex agents, and all other app-server messages queue in the socket buffer. With multiple concurrent agents or a talkative Codex session, the buffer can fill, causing Codex's send to back-pressure and stall. Consider dispatching each approval to a dedicated thread or using a structured-concurrency child task so the receive loop can keep draining.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/feed.md (1)

165-165: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update troubleshooting text to match current Codex telemetry behavior.

Line 165 says Feed only sees Codex permission hooks, but this doc now states Feed also records Codex PreToolUse telemetry. Please align this sentence with the updated behavior to avoid operator confusion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/feed.md` at line 165, The sentence claiming "Feed only sees Codex
permission hooks" is outdated; update the troubleshooting text to state that
Feed records both Codex permission hooks (e.g., request_user_input) and Codex
PreToolUse telemetry. Edit the line that currently mentions only permission
hooks so it references both types (permission hooks and PreToolUse) and clarify
that request_user_input is not a TUI hook but is captured as a permission hook,
while PreToolUse telemetry is also recorded by Feed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CLI/cmux.swift`:
- Around line 18567-18569: The code that constructs threadId calls
stringValue(in:params, keys: ["threadId", "thread_id"]) and then falls back to
stringValue(in:params, keys: ["threadID", "thread_id"]), redundantly checking
"thread_id" twice. Update the second call used for the fallback (the one
referencing "threadID") to only check the unchecked variant (e.g., keys:
["threadID"]) so the fallback sequence becomes stringValue(...
["threadId","thread_id"]) ?? stringValue(... ["threadID"]) ?? "unknown" (refer
to the threadId variable and the stringValue(in:params,keys:) calls).

---

Outside diff comments:
In `@docs/feed.md`:
- Line 165: The sentence claiming "Feed only sees Codex permission hooks" is
outdated; update the troubleshooting text to state that Feed records both Codex
permission hooks (e.g., request_user_input) and Codex PreToolUse telemetry. Edit
the line that currently mentions only permission hooks so it references both
types (permission hooks and PreToolUse) and clarify that request_user_input is
not a TUI hook but is captured as a permission hook, while PreToolUse telemetry
is also recorded by Feed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 05e2ceb3-1551-41f6-9714-af37e2ffc65e

📥 Commits

Reviewing files that changed from the base of the PR and between 970950f and dc3f0c9.

📒 Files selected for processing (4)
  • CLI/cmux.swift
  • Sources/Feed/FeedPermissionActionPolicy.swift
  • cmuxTests/FeedCoordinatorTests.swift
  • docs/feed.md

Comment thread CLI/cmux.swift Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d57d329f53

ℹ️ 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".

Comment thread CLI/cmux.swift Outdated
let cwd = stringValue(in: params, keys: ["cwd"])
let reason = stringValue(in: params, keys: ["reason"])
let command = stringValue(in: params, keys: ["command"])
let toolName = method == "item/fileChange/requestApproval" ? "Write" : "Bash"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include file diffs before approving Codex writes

When the app-server request is item/fileChange/requestApproval, this builds an actionable Feed card from only the approval request params and labels it as Write, but the Codex app-server docs state that the actual proposed edit details (changes/paths/diff) are carried on the preceding item/started fileChange item, while the approval request itself only carries ids/reason/grantRoot (https://github.com/openai/codex/blob/main/codex-rs/app-server/README.md#file-change-approvals). In file-change approval scenarios, Feed users can therefore approve a write without seeing what file or diff they are approving; cache/correlate the item/started fileChange by itemId and include those changes, or avoid surfacing these as actionable until the details are available.

Useful? React with 👍 / 👎.

Comment thread CLI/cmux.swift Outdated
Comment on lines +18646 to +18650
if codexTeamsModeRequestsPersistentApproval(mode),
codexTeamsAvailableDecisions(params).contains("acceptForSession") {
return "acceptForSession"
}
return "accept"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor Codex amendment decisions for Always Allow

When a Codex command approval advertises persistent choices such as acceptWithExecpolicyAmendment or applyNetworkPolicyAmendment, clicking Feed's newly-enabled Codex “Always Allow” falls through to plain accept unless acceptForSession is present. The app-server docs say clients should prefer availableDecisions and that command approvals can require those amendment object responses (https://github.com/openai/codex/blob/main/codex-rs/app-server/README.md#command-execution-approvals), so this path silently downgrades or may reject persistence for commands that need filesystem/network policy amendments; construct the advertised amendment response from the proposed amendment fields instead of returning accept.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmuxTests/FeedCoordinatorTests.swift`:
- Around line 36-55: The test only exercises absolute -C paths; add cases to
testCodexTeamsValidatesExplicitWorkingDirectoryExists that pass a relative -C
(e.g. a child folder name) and assert validateCodexTeamsWorkingDirectory uses
baseDirectory to resolve and validate the resulting path (mirroring
testCodexTeamsResolvesExplicitWorkingDirectoryFlags and the behavior in
codexTeamsResolvedWorkingDirectory); create the child directory under the
temporary baseDirectory, call CMUXCLI.validateCodexTeamsWorkingDirectory with
commandArgs ["-C", "<child>"] and baseDirectory set to the temp base, assert no
throw for existing child and XCTAssertThrowsError for a missing child path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: be2c8111-c8d2-4159-a461-ed9444134173

📥 Commits

Reviewing files that changed from the base of the PR and between dc3f0c9 and d57d329.

📒 Files selected for processing (2)
  • CLI/cmux.swift
  • cmuxTests/FeedCoordinatorTests.swift

Comment thread cmuxTests/FeedCoordinatorTests.swift Outdated
Comment on lines +36 to +55
func testCodexTeamsValidatesExplicitWorkingDirectoryExists() throws {
let existing = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-codex-teams-cwd-\(UUID().uuidString)", isDirectory: true)
try FileManager.default.createDirectory(at: existing, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: existing) }

XCTAssertNoThrow(
try CMUXCLI.validateCodexTeamsWorkingDirectory(
commandArgs: ["-C", existing.path],
baseDirectory: "/tmp"
)
)

XCTAssertThrowsError(
try CMUXCLI.validateCodexTeamsWorkingDirectory(
commandArgs: ["-C", existing.appendingPathComponent("missing").path],
baseDirectory: "/tmp"
)
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify working directory resolution and validation handle absolute vs relative paths consistently

# Search for the implementation of both functions
ast-grep --pattern $'func codexTeamsResolvedWorkingDirectory(commandArgs: $_, baseDirectory: $_) -> $_ {
  $$$
}'

ast-grep --pattern $'func validateCodexTeamsWorkingDirectory(commandArgs: $_, baseDirectory: $_) throws {
  $$$
}'

Repository: manaflow-ai/cmux

Length of output: 42


Clarify -C/--cd/--cwd working-directory semantics

  • codexTeamsResolvedWorkingDirectory treats absolute -C values as standardized absolute paths and intentionally ignores baseDirectory; validateCodexTeamsWorkingDirectory validates that resolved path exists and is a directory.
  • testCodexTeamsValidatesExplicitWorkingDirectoryExists covers the absolute branch; for parity with testCodexTeamsResolvesExplicitWorkingDirectoryFlags, add relative -C <child> validation cases against baseDirectory.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxTests/FeedCoordinatorTests.swift` around lines 36 - 55, The test only
exercises absolute -C paths; add cases to
testCodexTeamsValidatesExplicitWorkingDirectoryExists that pass a relative -C
(e.g. a child folder name) and assert validateCodexTeamsWorkingDirectory uses
baseDirectory to resolve and validate the resulting path (mirroring
testCodexTeamsResolvesExplicitWorkingDirectoryFlags and the behavior in
codexTeamsResolvedWorkingDirectory); create the child directory under the
temporary baseDirectory, call CMUXCLI.validateCodexTeamsWorkingDirectory with
commandArgs ["-C", "<child>"] and baseDirectory set to the temp base, assert no
throw for existing child and XCTAssertThrowsError for a missing child path.

Comment thread CLI/cmux.swift
Comment thread CLI/cmux.swift

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7ed2a21e71

ℹ️ 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".

enum FeedPermissionActionPolicy {
static func supportsPersistentPermissionModes(source: WorkstreamSource) -> Bool {
source != .codex && source != .hermesAgent
source != .hermesAgent

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Gate Codex Always Allow on advertised decisions

With this source-only policy, FeedPanelView shows “Always Allow” for every Codex approval, but Codex app-server requests can advertise the exact choices in availableDecisions (the app-server README says clients can use it to render the choices) and the new response helpers still fall back from Feed mode always to plain accept when no persistent Codex decision is available. In those command/file-change prompts, clicking “Always Allow” silently grants only a one-off approval; gate the persistent button on the specific request's available_decisions/grant data instead of enabling it for all Codex items.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
cmuxTests/FeedCoordinatorTests.swift (2)

10-10: 🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoff

Consider migrating to Swift Testing.

This test suite adds several new test methods while remaining on XCTest. Swift Testing is the current Apple-supported primitive for unit and integration tests. Since the suite is being actively expanded and doesn't appear to rely on XCTest-specific harnesses (unlike socket-based integration tests), migrating to Swift Testing would improve maintainability and align with project standards.

Migration template: XCTestCase subclass → @Suite struct; func testFoo() → @Test func foo(); XCTAssertEqual(a, b) → #expect(a == b); XCTUnwrap(x) → try #require(x).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxTests/FeedCoordinatorTests.swift` at line 10, Change the XCTest-based
suite to Swift Testing by replacing the XCTestCase subclass declaration
(FeedCoordinatorTests: XCTestCase) with an `@Suite` struct FeedCoordinatorTests
and add the Swift Testing import; convert each func testFoo() into `@Test` func
foo() and replace XCTest assertions (XCTAssertEqual, XCTAssertTrue, etc.) with
`#expect`(expression) equivalents and unwraps (XCTUnwrap) with try
`#require`(value); ensure any setUp/tearDown logic is ported into per-test setup
or Suite-level fixtures supported by Swift Testing and update test
names/signatures to throw if they used try/XCTUnwrap.

83-119: ⚠️ Potential issue | 🟠 Major | ⚖️ Poor tradeoff

Validate nested approval payload structure, not just field presence.

Lines 114-118 use XCTAssertNotNil to check that approval_params, additional_permissions, network_approval_context, command_actions, and proposed_execpolicy_amendment exist in tool_input, but do not validate their actual content. Similarly, line 119 only checks the type field of related_item, ignoring id, command, and cwd. These fields are part of the Codex app-server approval contract that Feed UI and decision mapping rely on. If codexTeamsFeedEvent drops a field or corrupts the structure, this test will still pass.

Validate the actual structure of at least one nested field to ensure the encoding is correct.

🔍 Example: Validate additional_permissions structure
 XCTAssertNotNil(toolInput["approval_params"])
-XCTAssertNotNil(toolInput["additional_permissions"])
+let additionalPermissions = try XCTUnwrap(toolInput["additional_permissions"] as? [String: Any])
+let fileSystem = try XCTUnwrap(additionalPermissions["fileSystem"] as? [String: Any])
+XCTAssertEqual(fileSystem["write"] as? [String], ["/tmp/project"])
 XCTAssertNotNil(toolInput["network_approval_context"])
 XCTAssertNotNil(toolInput["command_actions"])
 XCTAssertNotNil(toolInput["proposed_execpolicy_amendment"])
-XCTAssertEqual((toolInput["related_item"] as? [String: Any])?["type"] as? String, "commandExecution")
+let relatedItem = try XCTUnwrap(toolInput["related_item"] as? [String: Any])
+XCTAssertEqual(relatedItem["type"] as? String, "commandExecution")
+XCTAssertEqual(relatedItem["id"] as? String, "call-1")
+XCTAssertEqual(relatedItem["command"] as? String, "touch /tmp/cmux-security-review")
+XCTAssertEqual(relatedItem["cwd"] as? String, "/tmp/project")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxTests/FeedCoordinatorTests.swift` around lines 83 - 119, The test
currently only checks presence of nested keys in toolInput; update the
assertions to validate one or more nested structures' contents: replace the
XCTAssertNotNil checks for "approval_params", "additional_permissions",
"network_approval_context", "command_actions", and
"proposed_execpolicy_amendment" with concrete equality/structure checks (e.g.,
assert "additional_permissions" equals ["fileSystem": ["write":
["/tmp/project"]]], assert "network_approval_context" contains host ==
"example.com", assert "command_actions" is an array whose first element is
["type":"write","path":"/tmp/cmux-security-review"], and assert
"proposed_execpolicy_amendment" contains the expected prefix amendment). Also
extend the related_item assertion (currently only checking ["type"]) to assert
"id" == "call-1", "command" == "touch /tmp/cmux-security-review", and "cwd" ==
"/tmp/project" so the codexTeamsFeedEvent encoding is verified end-to-end.
CLI/cmux.swift (2)

18646-18685: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't embed raw approval payloads in tool_input.

approval_params and related_item copy whole upstream objects into the Feed event, and the pass-through fields below keep forwarding additional raw nested payloads. This bridge already lifts the user-visible bits it needs (item_id, command, cwd, decisions, etc.); keeping the original dictionaries means new upstream fields can get persisted/streamed unchanged, including patches or policy details that should stay redacted. Build an allowlisted summary instead of shipping the raw blobs.

Minimum local hardening
         var toolInput: [String: Any] = [
             "app_server_method": method,
             "request_id": requestIdString(requestId),
-            "item_id": itemId,
-            "approval_params": params
+            "item_id": itemId
         ]
@@
-        if let relatedItem {
-            toolInput["related_item"] = relatedItem
+        if let relatedItem {
             if command == nil,
                let relatedCommand = relatedItem["command"] as? String {
                 toolInput["command"] = relatedCommand

Based on learnings, workstream/feed payloads in this repo must be allowlisted and redact sensitive fields before persistence or streaming.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLI/cmux.swift` around lines 18646 - 18685, The code is embedding raw
upstream dictionaries into toolInput via "approval_params" and "related_item"
(and by copying passthrough fields) which risks leaking sensitive/unexpected
fields; replace these with allowlisted summaries: stop assigning params and
relatedItem directly, instead build a new summary dictionary (e.g. via helper
functions summarizeApprovalParams(params) and summarizeRelatedItem(relatedItem))
that only extracts the specific safe fields you already read (item_id, command,
cwd, decisions from codexTeamsDecisionNames, permissions,
network_approval_context, additional_permissions, command_actions,
proposed*_amendment, grant_root, approval_id, etc.) and put those summaries into
toolInput["approval_params"] and toolInput["related_item"]; reuse existing
helpers like stringValue(...) and codexTeamsDecisionNames(...) when populating
the summary, and ensure any unknown keys are omitted so raw upstream payloads
are never forwarded or persisted.

Source: Learnings


18745-18798: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail closed on unsupported Feed modes.

These helpers only special-case deny; every other string falls through to an approval response. If Feed returns "", "allow", or any future/invalid value, the bridge currently grants the Codex request instead of rejecting it. Gate on the known allow-set and treat everything else as deny/error.

Suggested fix
+    static let codexTeamsAllowedPermissionModes: Set<String> = ["once", "always", "all", "bypass"]
+
     static func codexTeamsCommandApprovalDecision(params: [String: Any], mode: String) -> Any {
-        if mode == "deny" { return "decline" }
+        guard codexTeamsAllowedPermissionModes.contains(mode) else { return "decline" }
         if codexTeamsModeRequestsPersistentApproval(mode),
            codexTeamsAvailableDecisions(params).contains("acceptForSession") {
             return "acceptForSession"
@@
     static func codexTeamsPermissionsApprovalResponse(params: [String: Any], mode: String) -> [String: Any] {
-        if mode == "deny" {
+        guard codexTeamsAllowedPermissionModes.contains(mode) else {
             return [
                 "permissions": [String: Any](),
                 "scope": "turn"
             ]
         }

Based on learnings, WorkstreamPermissionMode in this repo only permits once, always, all, bypass, and deny, and unknown values should fail closed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLI/cmux.swift` around lines 18745 - 18798, The helpers currently treat any
non-"deny" mode as allow — change to fail closed by validating mode against the
known WorkstreamPermissionMode allow-set
["once","always","all","bypass","deny"]; in codexTeamsCommandApprovalDecision
and codexTeamsFileChangeApprovalDecision add a top-level guard that if mode is
not in that set then behave as "deny" (return "decline" for
command/file-change); in codexTeamsPermissionsApprovalResponse do the same
(treat unknown mode as deny and return the same structure used when mode ==
"deny"); keep codexTeamsModeRequestsPersistentApproval as-is for
persistent-checks.

Source: Learnings

♻️ Duplicate comments (1)
cmuxTests/FeedCoordinatorTests.swift (1)

36-55: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add relative working-directory validation coverage.

The past review comment requesting relative -C path validation cases remains unaddressed. This test only exercises absolute paths; add cases that validate relative paths resolved against baseDirectory, mirroring the structure in testCodexTeamsResolvesExplicitWorkingDirectoryFlags.

📋 Suggested test expansion
 func testCodexTeamsValidatesExplicitWorkingDirectoryExists() throws {
     let existing = FileManager.default.temporaryDirectory
         .appendingPathComponent("cmux-codex-teams-cwd-\(UUID().uuidString)", isDirectory: true)
     try FileManager.default.createDirectory(at: existing, withIntermediateDirectories: true)
     defer { try? FileManager.default.removeItem(at: existing) }
+    
+    let child = existing.appendingPathComponent("child", isDirectory: true)
+    try FileManager.default.createDirectory(at: child, withIntermediateDirectories: true)

     XCTAssertNoThrow(
         try CMUXCLI.validateCodexTeamsWorkingDirectory(
             commandArgs: ["-C", existing.path],
             baseDirectory: "/tmp"
         )
     )
+    
+    XCTAssertNoThrow(
+        try CMUXCLI.validateCodexTeamsWorkingDirectory(
+            commandArgs: ["-C", "child"],
+            baseDirectory: existing.path
+        )
+    )

     XCTAssertThrowsError(
         try CMUXCLI.validateCodexTeamsWorkingDirectory(
             commandArgs: ["-C", existing.appendingPathComponent("missing").path],
             baseDirectory: "/tmp"
         )
     )
+    
+    XCTAssertThrowsError(
+        try CMUXCLI.validateCodexTeamsWorkingDirectory(
+            commandArgs: ["-C", "missing-child"],
+            baseDirectory: existing.path
+        )
+    )
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxTests/FeedCoordinatorTests.swift` around lines 36 - 55, Expand
testCodexTeamsValidatesExplicitWorkingDirectoryExists to include relative-path
cases: after creating the temporary directory referenced by `existing`, add
assertions that calling CMUXCLI.validateCodexTeamsWorkingDirectory with
commandArgs like ["-C", "<relative-path-to-existing>"] and baseDirectory set to
a directory that makes that relative path resolve to `existing` succeeds, and
that a relative path resolving to a non-existent subpath (e.g., "missing" under
that base) throws; use the same helper
`existing.appendingPathComponent("missing").path` logic but supply a relative
string and appropriate baseDirectory so the function
`CMUXCLI.validateCodexTeamsWorkingDirectory` is exercised for relative
resolution as in testCodexTeamsResolvesExplicitWorkingDirectoryFlags.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CLI/cmux.swift`:
- Around line 18329-18333: The resetConnectionSubscriptions() function currently
only clears subscribedThreadIds which leaves connection-scoped approval-item
cache (approvalItemById and approvalItemOrder) intact; update
resetConnectionSubscriptions() to also clear
approvalItemById.removeAll(keepingCapacity: true) and
approvalItemOrder.removeAll(keepingCapacity: true) while holding stateLock so
approval-item state is reset on reconnect and no stale relatedItem data leaks
into a new session.

In `@cmuxTests/FeedCoordinatorTests.swift`:
- Around line 206-214: The test only asserts the presence of
acceptWithExecpolicyAmendment but not that the proposedExecpolicyAmendment from
the params is preserved; update the assertion around
CMUXCLI.codexTeamsAppServerApprovalResponse to extract amendmentDecision and
then validate that the returned amendment structure includes the same
proposedExecpolicyAmendment (e.g., compare
amendmentDecision["acceptWithExecpolicyAmendment"] or
amendmentDecision["proposedExecpolicyAmendment"] against the original params
value) so the test fails if the builder drops or mutates the amendment.
- Around line 126-184: The test currently only checks presence of the nested
permissions payload; update
testCodexAppServerPermissionsApprovalBuildsFeedEventAndResponse to assert the
permissions content is preserved end-to-end: after creating permissions and
calling CMUXCLI.codexTeamsFeedEvent, unwrap event["tool_input"] and assert its
"permissions" (or nested "approval_params"->"permissions" if applicable) equals
the original permissions structure; then for the "once" (allow) response from
CMUXCLI.codexTeamsAppServerApprovalResponse, unwrap once["permissions"] and
assert it matches the original permissions dictionary (deep-equality), leaving
the existing "deny" empty assertion intact.

---

Outside diff comments:
In `@CLI/cmux.swift`:
- Around line 18646-18685: The code is embedding raw upstream dictionaries into
toolInput via "approval_params" and "related_item" (and by copying passthrough
fields) which risks leaking sensitive/unexpected fields; replace these with
allowlisted summaries: stop assigning params and relatedItem directly, instead
build a new summary dictionary (e.g. via helper functions
summarizeApprovalParams(params) and summarizeRelatedItem(relatedItem)) that only
extracts the specific safe fields you already read (item_id, command, cwd,
decisions from codexTeamsDecisionNames, permissions, network_approval_context,
additional_permissions, command_actions, proposed*_amendment, grant_root,
approval_id, etc.) and put those summaries into toolInput["approval_params"] and
toolInput["related_item"]; reuse existing helpers like stringValue(...) and
codexTeamsDecisionNames(...) when populating the summary, and ensure any unknown
keys are omitted so raw upstream payloads are never forwarded or persisted.
- Around line 18745-18798: The helpers currently treat any non-"deny" mode as
allow — change to fail closed by validating mode against the known
WorkstreamPermissionMode allow-set ["once","always","all","bypass","deny"]; in
codexTeamsCommandApprovalDecision and codexTeamsFileChangeApprovalDecision add a
top-level guard that if mode is not in that set then behave as "deny" (return
"decline" for command/file-change); in codexTeamsPermissionsApprovalResponse do
the same (treat unknown mode as deny and return the same structure used when
mode == "deny"); keep codexTeamsModeRequestsPersistentApproval as-is for
persistent-checks.

In `@cmuxTests/FeedCoordinatorTests.swift`:
- Line 10: Change the XCTest-based suite to Swift Testing by replacing the
XCTestCase subclass declaration (FeedCoordinatorTests: XCTestCase) with an
`@Suite` struct FeedCoordinatorTests and add the Swift Testing import; convert
each func testFoo() into `@Test` func foo() and replace XCTest assertions
(XCTAssertEqual, XCTAssertTrue, etc.) with `#expect`(expression) equivalents and
unwraps (XCTUnwrap) with try `#require`(value); ensure any setUp/tearDown logic is
ported into per-test setup or Suite-level fixtures supported by Swift Testing
and update test names/signatures to throw if they used try/XCTUnwrap.
- Around line 83-119: The test currently only checks presence of nested keys in
toolInput; update the assertions to validate one or more nested structures'
contents: replace the XCTAssertNotNil checks for "approval_params",
"additional_permissions", "network_approval_context", "command_actions", and
"proposed_execpolicy_amendment" with concrete equality/structure checks (e.g.,
assert "additional_permissions" equals ["fileSystem": ["write":
["/tmp/project"]]], assert "network_approval_context" contains host ==
"example.com", assert "command_actions" is an array whose first element is
["type":"write","path":"/tmp/cmux-security-review"], and assert
"proposed_execpolicy_amendment" contains the expected prefix amendment). Also
extend the related_item assertion (currently only checking ["type"]) to assert
"id" == "call-1", "command" == "touch /tmp/cmux-security-review", and "cwd" ==
"/tmp/project" so the codexTeamsFeedEvent encoding is verified end-to-end.

---

Duplicate comments:
In `@cmuxTests/FeedCoordinatorTests.swift`:
- Around line 36-55: Expand
testCodexTeamsValidatesExplicitWorkingDirectoryExists to include relative-path
cases: after creating the temporary directory referenced by `existing`, add
assertions that calling CMUXCLI.validateCodexTeamsWorkingDirectory with
commandArgs like ["-C", "<relative-path-to-existing>"] and baseDirectory set to
a directory that makes that relative path resolve to `existing` succeeds, and
that a relative path resolving to a non-existent subpath (e.g., "missing" under
that base) throws; use the same helper
`existing.appendingPathComponent("missing").path` logic but supply a relative
string and appropriate baseDirectory so the function
`CMUXCLI.validateCodexTeamsWorkingDirectory` is exercised for relative
resolution as in testCodexTeamsResolvesExplicitWorkingDirectoryFlags.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0de5aea3-8f03-433b-bb9e-26a2a83d09ca

📥 Commits

Reviewing files that changed from the base of the PR and between d57d329 and 7ed2a21.

📒 Files selected for processing (2)
  • CLI/cmux.swift
  • cmuxTests/FeedCoordinatorTests.swift

Comment thread CLI/cmux.swift
Comment on lines +18329 to +18333
private func resetConnectionSubscriptions() {
stateLock.lock()
subscribedThreadIds.removeAll(keepingCapacity: true)
stateLock.unlock()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear the approval-item cache on reconnect.

resetConnectionSubscriptions() only drops subscribedThreadIds. approvalItemById and approvalItemOrder survive a new app-server connection, so a reused item id can attach a stale relatedItem from the previous session to a fresh approval request. That can show the wrong command/cwd/patch in Feed and leak prior-session context.

Suggested fix
         private func resetConnectionSubscriptions() {
             stateLock.lock()
-            subscribedThreadIds.removeAll(keepingCapacity: true)
-            stateLock.unlock()
+            defer { stateLock.unlock() }
+            subscribedThreadIds.removeAll(keepingCapacity: true)
+            approvalItemById.removeAll(keepingCapacity: true)
+            approvalItemOrder.removeAll(keepingCapacity: true)
         }

Based on the reconnect flow in this file, the approval-item cache is connection-scoped state and should be reset with the subscription set.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLI/cmux.swift` around lines 18329 - 18333, The
resetConnectionSubscriptions() function currently only clears
subscribedThreadIds which leaves connection-scoped approval-item cache
(approvalItemById and approvalItemOrder) intact; update
resetConnectionSubscriptions() to also clear
approvalItemById.removeAll(keepingCapacity: true) and
approvalItemOrder.removeAll(keepingCapacity: true) while holding stateLock so
approval-item state is reset on reconnect and no stale relatedItem data leaks
into a new session.

Comment thread cmuxTests/FeedCoordinatorTests.swift Outdated
Comment on lines +126 to +184
func testCodexAppServerPermissionsApprovalBuildsFeedEventAndResponse() throws {
let permissions: [String: Any] = [
"network": ["enabled": true],
"fileSystem": [
"read": ["/tmp/read"],
"write": ["/tmp/write"]
]
]
let event = CMUXCLI.codexTeamsFeedEvent(
method: "item/permissions/requestApproval",
requestId: "permissions-request",
params: [
"threadId": "thread-1",
"turnId": "turn-1",
"itemId": "permissions-call",
"environmentId": "local",
"cwd": "/tmp/project",
"reason": "Need broader access",
"permissions": permissions
],
workspaceId: "workspace-1"
)

XCTAssertEqual(event["tool_name"] as? String, "request_permissions")
XCTAssertEqual(event["_opencode_request_id"] as? String, "codex-app-server-permissions-call")
let toolInput = try XCTUnwrap(event["tool_input"] as? [String: Any])
XCTAssertEqual(toolInput["app_server_method"] as? String, "item/permissions/requestApproval")
XCTAssertNotNil(toolInput["approval_params"])
XCTAssertNotNil(toolInput["permissions"])

let once = try XCTUnwrap(
CMUXCLI.codexTeamsAppServerApprovalResponse(
method: "item/permissions/requestApproval",
params: ["permissions": permissions],
mode: "once"
)
)
XCTAssertEqual(once["scope"] as? String, "turn")
XCTAssertNotNil(once["permissions"])

let always = try XCTUnwrap(
CMUXCLI.codexTeamsAppServerApprovalResponse(
method: "item/permissions/requestApproval",
params: ["permissions": permissions],
mode: "always"
)
)
XCTAssertEqual(always["scope"] as? String, "session")

let deny = try XCTUnwrap(
CMUXCLI.codexTeamsAppServerApprovalResponse(
method: "item/permissions/requestApproval",
params: ["permissions": permissions],
mode: "deny"
)
)
XCTAssertEqual(deny["scope"] as? String, "turn")
XCTAssertEqual((deny["permissions"] as? [String: Any])?.isEmpty, true)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate permissions payload structure in event and response.

This test builds a nested permissions structure (lines 127-133) but only checks its presence in the event via XCTAssertNotNil (line 154). Similarly, the response validation for once mode (line 164) checks that permissions is not nil but doesn't verify the actual content matches the input structure. Only the deny case (line 183) validates the payload (asserting it's empty).

Validate that the permissions structure flows correctly from params → event tool_input → response payload for at least one allow case.

🔍 Suggested validation
 let toolInput = try XCTUnwrap(event["tool_input"] as? [String: Any])
 XCTAssertEqual(toolInput["app_server_method"] as? String, "item/permissions/requestApproval")
 XCTAssertNotNil(toolInput["approval_params"])
-XCTAssertNotNil(toolInput["permissions"])
+let eventPermissions = try XCTUnwrap(toolInput["permissions"] as? [String: Any])
+let eventFileSystem = try XCTUnwrap(eventPermissions["fileSystem"] as? [String: Any])
+XCTAssertEqual(eventFileSystem["write"] as? [String], ["/tmp/write"])
+XCTAssertEqual(eventFileSystem["read"] as? [String], ["/tmp/read"])

 let once = try XCTUnwrap(
     CMUXCLI.codexTeamsAppServerApprovalResponse(
         method: "item/permissions/requestApproval",
         params: ["permissions": permissions],
         mode: "once"
     )
 )
 XCTAssertEqual(once["scope"] as? String, "turn")
-XCTAssertNotNil(once["permissions"])
+let oncePermissions = try XCTUnwrap(once["permissions"] as? [String: Any])
+let onceFileSystem = try XCTUnwrap(oncePermissions["fileSystem"] as? [String: Any])
+XCTAssertEqual(onceFileSystem["write"] as? [String], ["/tmp/write"])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func testCodexAppServerPermissionsApprovalBuildsFeedEventAndResponse() throws {
let permissions: [String: Any] = [
"network": ["enabled": true],
"fileSystem": [
"read": ["/tmp/read"],
"write": ["/tmp/write"]
]
]
let event = CMUXCLI.codexTeamsFeedEvent(
method: "item/permissions/requestApproval",
requestId: "permissions-request",
params: [
"threadId": "thread-1",
"turnId": "turn-1",
"itemId": "permissions-call",
"environmentId": "local",
"cwd": "/tmp/project",
"reason": "Need broader access",
"permissions": permissions
],
workspaceId: "workspace-1"
)
XCTAssertEqual(event["tool_name"] as? String, "request_permissions")
XCTAssertEqual(event["_opencode_request_id"] as? String, "codex-app-server-permissions-call")
let toolInput = try XCTUnwrap(event["tool_input"] as? [String: Any])
XCTAssertEqual(toolInput["app_server_method"] as? String, "item/permissions/requestApproval")
XCTAssertNotNil(toolInput["approval_params"])
XCTAssertNotNil(toolInput["permissions"])
let once = try XCTUnwrap(
CMUXCLI.codexTeamsAppServerApprovalResponse(
method: "item/permissions/requestApproval",
params: ["permissions": permissions],
mode: "once"
)
)
XCTAssertEqual(once["scope"] as? String, "turn")
XCTAssertNotNil(once["permissions"])
let always = try XCTUnwrap(
CMUXCLI.codexTeamsAppServerApprovalResponse(
method: "item/permissions/requestApproval",
params: ["permissions": permissions],
mode: "always"
)
)
XCTAssertEqual(always["scope"] as? String, "session")
let deny = try XCTUnwrap(
CMUXCLI.codexTeamsAppServerApprovalResponse(
method: "item/permissions/requestApproval",
params: ["permissions": permissions],
mode: "deny"
)
)
XCTAssertEqual(deny["scope"] as? String, "turn")
XCTAssertEqual((deny["permissions"] as? [String: Any])?.isEmpty, true)
}
func testCodexAppServerPermissionsApprovalBuildsFeedEventAndResponse() throws {
let permissions: [String: Any] = [
"network": ["enabled": true],
"fileSystem": [
"read": ["/tmp/read"],
"write": ["/tmp/write"]
]
]
let event = CMUXCLI.codexTeamsFeedEvent(
method: "item/permissions/requestApproval",
requestId: "permissions-request",
params: [
"threadId": "thread-1",
"turnId": "turn-1",
"itemId": "permissions-call",
"environmentId": "local",
"cwd": "/tmp/project",
"reason": "Need broader access",
"permissions": permissions
],
workspaceId: "workspace-1"
)
XCTAssertEqual(event["tool_name"] as? String, "request_permissions")
XCTAssertEqual(event["_opencode_request_id"] as? String, "codex-app-server-permissions-call")
let toolInput = try XCTUnwrap(event["tool_input"] as? [String: Any])
XCTAssertEqual(toolInput["app_server_method"] as? String, "item/permissions/requestApproval")
XCTAssertNotNil(toolInput["approval_params"])
let eventPermissions = try XCTUnwrap(toolInput["permissions"] as? [String: Any])
let eventFileSystem = try XCTUnwrap(eventPermissions["fileSystem"] as? [String: Any])
XCTAssertEqual(eventFileSystem["write"] as? [String], ["/tmp/write"])
XCTAssertEqual(eventFileSystem["read"] as? [String], ["/tmp/read"])
let once = try XCTUnwrap(
CMUXCLI.codexTeamsAppServerApprovalResponse(
method: "item/permissions/requestApproval",
params: ["permissions": permissions],
mode: "once"
)
)
XCTAssertEqual(once["scope"] as? String, "turn")
let oncePermissions = try XCTUnwrap(once["permissions"] as? [String: Any])
let onceFileSystem = try XCTUnwrap(oncePermissions["fileSystem"] as? [String: Any])
XCTAssertEqual(onceFileSystem["write"] as? [String], ["/tmp/write"])
let always = try XCTUnwrap(
CMUXCLI.codexTeamsAppServerApprovalResponse(
method: "item/permissions/requestApproval",
params: ["permissions": permissions],
mode: "always"
)
)
XCTAssertEqual(always["scope"] as? String, "session")
let deny = try XCTUnwrap(
CMUXCLI.codexTeamsAppServerApprovalResponse(
method: "item/permissions/requestApproval",
params: ["permissions": permissions],
mode: "deny"
)
)
XCTAssertEqual(deny["scope"] as? String, "turn")
XCTAssertEqual((deny["permissions"] as? [String: Any])?.isEmpty, true)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxTests/FeedCoordinatorTests.swift` around lines 126 - 184, The test
currently only checks presence of the nested permissions payload; update
testCodexAppServerPermissionsApprovalBuildsFeedEventAndResponse to assert the
permissions content is preserved end-to-end: after creating permissions and
calling CMUXCLI.codexTeamsFeedEvent, unwrap event["tool_input"] and assert its
"permissions" (or nested "approval_params"->"permissions" if applicable) equals
the original permissions structure; then for the "once" (allow) response from
CMUXCLI.codexTeamsAppServerApprovalResponse, unwrap once["permissions"] and
assert it matches the original permissions dictionary (deep-equality), leaving
the existing "deny" empty assertion intact.

Comment thread cmuxTests/FeedCoordinatorTests.swift Outdated
Comment thread CLI/cmux.swift

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 851b3378d6

ℹ️ 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".

Comment thread CLI/cmux.swift
Comment on lines +18411 to +18415
private func pushCodexApprovalToFeed(event: [String: Any]) throws -> [String: Any] {
try socketClient.sendV2(method: "feed.push", params: [
"event": event,
"wait_timeout_seconds": 120
], responseTimeout: 125)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle native Codex approval resolution

When the Codex TUI resolves the same app-server approval (the fallback path described here for timed-out/no-decision Feed prompts), the watcher is blocked inside this 120s feed.push wait and never processes the app-server serverRequest/resolved cleanup notification. In that case the Feed card remains pending/actionable after Codex has already proceeded, and a later Feed click can send a stale response for a request that was already resolved; keep reading/resolving app-server cleanup while the Feed prompt is open or cancel the Feed waiter when serverRequest/resolved arrives.

Useful? React with 👍 / 👎.

Comment thread CLI/cmux.swift Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5f63044ace

ℹ️ 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".

Comment thread CLI/cmux.swift Outdated
Comment on lines +18357 to +18360
try connection.respondError(
requestId: requestId,
code: -32601,
message: "cmux Codex Teams watcher does not handle \(method)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not reject non-approval app-server requests

When a subscribed Codex thread triggers another server-initiated request (for example mcpServer/elicitation/request or item/tool/call, which the app-server README documents as client-handled JSON-RPC requests), this watcher now sends -32601 for every request that is not one of the approval methods. In those threads, the watcher is an initialized/subscribed client, so this error response can cause the elicitation or dynamic tool call to fail instead of letting the interactive Codex client handle it; avoid subscribing to request types the watcher cannot service or handle/forward them rather than rejecting them.

Useful? React with 👍 / 👎.

Comment thread CLI/cmux.swift

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c36c8bb. Configure here.

Comment thread CLI/CodexTeamsApprovalBridge.swift Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c36c8bb664

ℹ️ 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".

Comment thread CLI/CodexTeamsApprovalBridge.swift Outdated
Comment on lines +121 to +124
case "item/commandExecution/requestApproval", "item/fileChange/requestApproval":
return ["decision": noDecisionApprovalDecision(params: params)]
case "item/permissions/requestApproval":
return permissionsApprovalResponse(params: params, mode: "deny")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid canceling approvals on Feed timeout

In Codex Teams, handleApprovalRequest calls this no-decision path when feed.push fails or returns without a Feed click; mapping that case to cancel/decline (or an empty permissions grant) is still an explicit app-server response, so the root Codex TUI cannot fall back to its native approval prompt and the command/edit/permission request is denied or canceled without a user decision. This affects any approval where Feed is unavailable or times out; avoid answering the app-server request as a negative decision when the Feed bridge has no decision.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bb19d16ff3

ℹ️ 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".

Comment thread CLI/cmux.swift
try handleApprovalRequest(message, method: method, requestId: requestId, connection: connection) {
return
}
if message["id"] != nil { return }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Respond to non-approval app-server requests

Fresh evidence in this revision is that this branch now drops every server-initiated request with an id that is not one of the approval methods, without sending any JSON-RPC result or error. For subscribed Codex threads that emit other app-server requests such as mcpServer/elicitation/request, item/tool/call, or attestation generation, the app-server expects the connected client to answer; leaving the request unanswered can stall the active turn until timeout instead of letting the interactive client proceed. Either avoid subscribing this watcher to request types it cannot service, or forward/respond to them explicitly.

Useful? React with 👍 / 👎.

Comment thread CLI/cmux.swift
Comment on lines +18469 to +18472
if approvalIsSuppressed(suppressionKey) {
fputs("cmux codex-teams watcher leaving previously unresolved approval \(suppressionKey) to native Codex\n", stderr)
return true
}

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.

P1 Suppression keys survive watcher reconnects, silently consuming re-presented approvals

resetConnectionSubscriptions() clears subscribedThreadIds on reconnect but leaves suppressedApprovalKeys intact. If Codex's app-server re-presents a previously-timed-out approval on the fresh watcher connection — with the same approvalId/itemId but a new JSON-RPC requestId — this guard fires, returns true (consumed), and never calls connection.respond(...). The Codex app-server now has an unanswered JSON-RPC request open on a live connection, so Codex's native TUI cannot step in unless Codex has its own per-request timeout. Because resetConnectionSubscriptions is the natural reset boundary, clearing suppressedApprovalKeys there would let re-presented approvals be forwarded to Feed again on the fresh connection rather than stalling indefinitely.

@lawrencecchen
lawrencecchen merged commit ed1744e into main Jun 8, 2026
25 checks passed
@lawrencecchen
lawrencecchen deleted the feat-codex-permission-hook branch June 8, 2026 08:33
austinywang added a commit that referenced this pull request Aug 7, 2026
…erts

Review findings on the previous commit:

- The notification body carried the full tool summary (complete shell
  command). Commands can embed credentials, and notification banners reach
  lock screens, paired phones, and the recorded notification history. The
  body now names only the tool — the same "<tool> needs approval" string
  the in-app Feed approval banner uses — never the tool input.

- Codex fires PermissionRequest before its own "Approve for me" reviewer
  (#5507), so an auto-approved request would leave a stale or false
  "Permission" alert with nothing pending. Codex tool lifecycle progress
  (PreToolUse/PostToolUse feed events) now clears the pane's notifications,
  mirroring Claude's pre-tool-use clear_notifications contract. The clear is
  registry-scoped to sources that raise native approval prompts, so other
  agents' tool telemetry never touches the notification queue.

The immediate notify on PermissionRequest is retained deliberately: codex
has no post-reviewer hook, and the wrapper-injected schema already posts
this same immediate needs-permission notification via `hooks codex
notification`; suppressing until authoritative proof would recreate the
silence reported in #9592.

Verified against a mock socket: PermissionRequest now emits
`notify_target_async <ws> <sf> Codex|Permission|shell needs approval|c=needs-permission;p=0`
and PreToolUse/PostToolUse emit
`clear_notifications --tab=<ws> --panel=<sf>`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview – cmux — bb19d16f Deployed Jun 8, 2026 by vercel[bot]
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.

1 participant