Skip to content

Make Codex hooks fire-and-forget - #6110

Merged
lawrencecchen merged 7 commits into
mainfrom
feat-codex-hook-timeouts
Jun 14, 2026
Merged

lawrencecchen merged 7 commits into
mainfrom
feat-codex-hook-timeouts

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • make generated Codex hooks spool stdin and dispatch cmux in the background
  • replace stale synchronous Codex hook commands during install
  • add regression coverage for duplicate cleanup and timeout behavior

Root cause

~/.codex/hooks.json still had an older synchronous cmux hook installed after the newer fire-and-forget wrapper. Codex ran both, and the synchronous command could block on the cmux socket long enough to exceed Codex's 5s hook timeout.

Verification

  • xcodebuild -project cmux.xcodeproj -scheme cmux-unit -configuration Debug -destination 'platform=macOS,arch=arm64' -derivedDataPath /tmp/cmux-codex-hooks-test -only-testing:cmuxTests/CLINotifyProcessIntegrationRegressionTests/testCodexHookInstallPrefersLaunchingAppBundledCLI -only-testing:cmuxTests/CLINotifyProcessIntegrationRegressionTests/testCodexInstalledHookReturnsBeforeSlowCmuxCommandFinishes test

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


Note

Medium Risk
Changes Codex hook install scripts, async subprocess behavior, and session/UI mutation paths; regression tests are broad but race timing in production hooks remains possible.

Overview
Codex session-start and prompt-submit hooks installed via hooks.json now use a fire-and-forget shell wrapper: hook stdin is spooled to a temp file, cmux runs in the background (nohup + 30s watchdog), and the hook immediately prints {} so Codex does not hit its ~5s synchronous timeout. stop and feed hooks stay blocking.

The CLI hook handlers add Codex-specific freshness rules: ignore stale session-start when active turn state exists or a completed turn would be revived by the same PID; ignore prompt-submit for terminal turns and skip visible UI / diff-baseline updates when late async work races ahead. Session store APIs (upsertCodexSessionStartIfFresh, upsertCodexPromptRunningIfFresh, recordPromptSubmit with rejectTerminalTurn) back those checks. Agent PID resolution prefers CMUX_CODEX_PID (and analogous env vars) over PPID inference.

CLICodexHookTimeoutRegressionTests covers install dedupe/replace of legacy sync commands, fast hook return vs slow cmux, and stale session/prompt behavior; an existing integration test now expects terminal turns not to refresh last-turn diff baselines.

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


Summary by cubic

Make Codex session-start and prompt-submit fire-and-forget. Hooks print {} immediately while cmux runs in the background with a 30s watchdog, and stricter staleness checks ensure late Codex events don’t change state or UI; terminal turn tombstones are preserved and stale/terminal prompt baseline writes are skipped.

  • Bug Fixes
    • Routing/install: Codex-only async wrapper for session-start/prompt-submit; stop and feed hooks stay synchronous; installer replaces legacy sync commands and dedupes ~/.codex/hooks.json.
    • Wrapper details: spools stdin to a temp file, launches via nohup, passes --socket when CMUX_SOCKET_PATH is set, exports CMUX_CODEX_PID, echoes {}, and enforces a 30s watchdog.
    • PID attribution: CLI now prefers CMUX_*_PID over PPID for all agents (Codex included).
    • Freshness rules: new upsertCodexSessionStartIfFresh and upsertCodexPromptRunningIfFresh guard late updates; session-start ignored if any active turn exists, or if only completed-turn state exists and the incoming PID matches; fresh starts clear completed-turn state without removing terminal turn tombstones.
    • Ordering and baseline: re-check staleness after accept and before baseline/UI/telemetry; abort early and restore UI from persisted state if a turn becomes terminal; skip Codex prompt diff baseline writes for stale/terminal turns; recordPromptSubmit now returns (staleTerminalTurn, nested) to avoid accidental UI changes.
    • Tests: CLICodexHookTimeoutRegressionTests cover install replace/dedupe, fast return vs slow cmux, stale prompt-submit/session-start handling, active-turn protection, fresh-start behavior, and same-PID completed-turn starts; updated prompt baseline test ensures terminal turns don’t refresh the last-turn baseline.

Written for commit 6c8aef3. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Codex hook shell commands can now run in a fire-and-forget mode when installed and invoked.
    • Codex-specific routing was added for hook execution paths.
  • Bug Fixes

    • Fixed Codex prompt-submit handling to avoid acting on stale terminal turns.
    • Prevented hook invocations from hanging by enforcing an execution timeout/kill safeguard.
  • Tests

    • Added serialized regression coverage for hook installation replacement, fast completion vs slow execution behavior, and stale-turn protection.

@vercel

vercel Bot commented Jun 14, 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 14, 2026 10:29am
cmux-staging Building Building Preview, Comment Jun 14, 2026 10:29am

@coderabbitai

coderabbitai Bot commented Jun 14, 2026 •

Copy link
Copy Markdown

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

agentHookShellCommand is updated to special-case def.name == "codex" and route to a new codexFireAndForgetAgentHookShellCommand method that generates a nohup/background shell snippet with executable discovery, temp payload file handling, optional socket support, and 30-second watchdog. The codex prompt-submit command path adds an early-return guard to detect stale terminal turns and exit immediately without reviving stopped sessions. Two new Swift files are registered in the Xcode project, and three regression tests verify hook installation replacement, non-blocking hook return during async cmux execution, and session state isolation.

Changes

Codex Fire-and-Forget Hook Dispatch

Layer / File(s) Summary
Codex fire-and-forget shell command dispatch
CLI/CMUXCLI+AgentHookDefinitions.swift, CLI/CMUXCLI+CodexFireAndForgetHooks.swift
agentHookShellCommand adds an early-return for def.name == "codex" that delegates to codexFireAndForgetAgentHookShellCommand, which generates a nohup/background shell snippet with executable discovery (CMUX_BUNDLED_CLI_PATH or command -v), prerequisite validation (CMUX_SURFACE_ID, disable env var), temp payload file, optional --socket support, 30-second watchdog, and {} fallback output.
Stale terminal turn early-return guard
CLI/cmux.swift
The codex prompt-submit flow now derives incomingTurnId and builds a terminalPromptTurnIds set; a new early-return guard detects stale (already-terminal) turns, emits a telemetry breadcrumb, prints an empty payload, and exits immediately to prevent reviving stopped sessions.
Xcode project build configuration
cmux.xcodeproj/project.pbxproj
Adds PBXBuildFile, PBXFileReference, group membership, and PBXSourcesBuildPhase entries for CMUXCLI+CodexFireAndForgetHooks.swift (CLI group, cmux-cli target) and CLICodexHookTimeoutRegressionTests.swift (cmuxTests group, cmuxTests target).
Test suite foundation and core utilities
cmuxTests/CLICodexHookTimeoutRegressionTests.swift (lines 1–14, 192–246)
Defines the serialized test suite container and ProcessRunResult type for capturing subprocess exit status and timeouts. Adds bundledCLIPath() to locate the test CLI executable, codexHookTestEnvironment(root:codexHome:) to construct constrained subprocess environments, codexHookCommands(in:) to extract hook commands from hooks.json, and makeExecutableShellFile(at:lines:) to write test shell scripts.
Hook installation replacement regression test
cmuxTests/CLICodexHookTimeoutRegressionTests.swift (lines 15–51)
codexHookInstallReplacesSynchronousBundledHook seeds a temporary Codex home with a legacy synchronous prompt-submit hook, runs the bundled hooks codex install --yes installer, and verifies the stale command is removed and replaced with a single collapsed nohup/payload-piped command. Also confirms Codex feed hooks are preserved with expected filtering.
Async execution and non-blocking return regression test
cmuxTests/CLICodexHookTimeoutRegressionTests.swift (lines 53–109, 372–434, 425–434)
codexInstalledHookReturnsBeforeSlowCmuxCommandFinishes creates a fake cmux script that captures CLI args and stdin to temp files then sleeps, installs the Codex prompt-submit hook, runs the installed hook via /bin/sh -c with JSON payload on stdin, and asserts immediate {} return before the fake cmux sleep finishes, expected args and payload delivery, and eventual file marker appearance. Uses runProcess() for subprocess execution with stdout/stderr capture and timeout detection, and waitForFile() for file polling.
Socket and session state isolation regression test
cmuxTests/CLICodexHookTimeoutRegressionTests.swift (lines 110–190, 247–371)
codexPromptSubmitDoesNotReviveStoppedTurn sets up a mock Unix-domain socket server via bindUnixSocket() and startMockSocketServerAccepting(), seeds a hook session state file, runs hooks codex prompt-submit against the socket, and asserts immediate {} return, absence of "resume/running/clear notification" command patterns, unchanged idle session lifecycle/runtime status, and preserved terminal prompt turn IDs. Includes CapturedSocketCommands (thread-safe command capture), makeSocketPath() for temp socket generation, and handleMockSocketClient() for mocked JSON request handling.

Sequence Diagram

sequenceDiagram
  participant Caller as Agent Hook Caller
  participant HookCmd as agentHookShellCommand()
  participant CodexCmd as codexFireAndForgetAgentHookShellCommand()
  participant Shell as Shell Execution
  participant cmux as cmux CLI

  Caller->>HookCmd: invoke(def.name="codex", command)
  HookCmd->>CodexCmd: generate shell snippet
  CodexCmd->>CodexCmd: check prerequisites<br/>(CMUX_SURFACE_ID, disable env var)
  CodexCmd->>CodexCmd: resolve cmux executable
  CodexCmd->>CodexCmd: create temp payload file
  CodexCmd-->>Caller: return shell snippet<br/>(immediate, non-blocking)
  Caller->>Shell: sh -c snippet
  Shell->>cmux: nohup cmux --socket ...<br/>hooks codex prompt-submit < payload &
  Shell->>Shell: arm 30-second watchdog
  Shell-->>Caller: {} (instant return)
  cmux->>cmux: execute asynchronously
  Shell->>Shell: wait briefly, kill watchdog,<br/>rm payload
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • manaflow-ai/cmux#4583: Both PRs modify CLI/cmux.swift's Codex prompt-submit flow around terminal turn_id/terminal active prompt turn handling (state/guard logic and related tests), so the changes are directly related at the hook/turn-notification code level.
  • manaflow-ai/cmux#4409: Both PRs touch CMUXCLI+AgentHookDefinitions.swift's agentHookShellCommand(_:for:) around the pinned-hook dispatch path (main PR refactors its usesPinnedHookDispatch early-return, while the other PR generalizes pinned-hook routing for grok/antigravity).
  • manaflow-ai/cmux#5507: Both PRs change Codex hook behavior to make it non-blocking and correct timeout handling—main PR adds Codex-specific "fire-and-forget" hook command dispatch, while the retrieved PR updates Codex permission/Feed hook semantics and Codex timeout normalization—so they're directly related at the Codex hook execution layer.

Poem

🐇 Hop, hop, the hook must fire and go,
No waiting for the response to show.
A nohup shell darts through the night,
Returns {} in a flash of light!
The watchdog barks at thirty ticks,
Then cleanup hops—no hangs or tricks! 🎉

🚥 Pre-merge checks | ✅ 19 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The PR description covers Summary, Root cause, and Verification, but is missing the Testing, Demo Video, Review Trigger, and Checklist sections from the required template. Add the Testing section explaining how the change was tested, the Demo Video section (if applicable), the Review Trigger block, and complete the Checklist with all items checked or appropriately addressed.
✅ Passed checks (19 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: making Codex hooks fire-and-forget (asynchronous background execution instead of synchronous blocking).
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 No Swift 6 actor isolation violations introduced; new extension method is a pure static function, test file properly excluded, CLI-only context with no UI threading concerns.
Cmux Swift Blocking Runtime ✅ Passed Production Swift code introduces no blocking primitives: new codexFireAndForgetAgentHookShellCommand returns a shell command string (sleep runs in backgrounded subprocess), AgentHookDefinitions has...
Cmux Expensive Synchronous Load ✅ Passed PR adds no expensive synchronous loaders to main actor/interactive paths; stale-turn guard uses lightweight store.lookup (dict access), not RestorableAgentSessionIndex.load().
Cmux Cache Substitution Correctness ✅ Passed The PR correctly reads fresh session state from disk via store.lookup() before checking terminalPromptTurnIds; no cached values substitute for authoritative reads in persistence paths.
Cmux No Hacky Sleeps ✅ Passed All modified files are Swift (.swift) or Xcode project files. The check explicitly excludes Swift code ("Swift timing and blocking primitives are covered by swift-blocking-runtime.md"). No TypeSc...
Cmux Algorithmic Complexity ✅ Passed All production code changes comply with algorithmic complexity rules. The stale-terminal-turn check uses a Set bounded to 32 items with explicit documentation (maxRememberedTerminalPromptTurnIds),...
Cmux Swift Concurrency ✅ Passed PR introduces no legacy Swift concurrency in production code. Fire-and-forget behavior implemented as shell command strings, not Swift Tasks or DispatchQueue. Test-only DispatchQueue usage is permi...
Cmux Swift @Concurrent ✅ Passed All Swift changes are synchronous functions performing string manipulation and control flow logic; no async/await code, nonisolated async functions, or @concurrent annotations were introduced.
Cmux Swift File And Package Boundaries ✅ Passed All changes comply with Swift file/package boundaries rules. New CMUXCLI+CodexFireAndForgetHooks.swift is 6 lines with single focused responsibility (shell command generation). Test file CLICodexHo...
Cmux Swift Logging ✅ Passed All logging changes comply with swift-logging.md rules: print() statements are CLI hook output (allowed), no NSLog/debugPrint/dump() violations found, tests included, no secrets exposed.
Cmux User-Facing Error Privacy ✅ Passed PR produces no user-facing errors or output that violates privacy rules. User output is only '{}' (internal protocol). Environment variables used only in shell script config, not exposed to users....
Cmux Full Internationalization ✅ Passed No user-facing strings requiring localization. All outputs are protocol responses ({}), telemetry markers, literal command tokens, test code, or build configuration—all explicitly allowed by the in...
Cmux Swiftui State Layout ✅ Passed PR contains no SwiftUI code—all changes are CLI hook implementations and tests. SwiftUI check is not applicable.
Cmux Architecture Rethink ✅ Passed PR uses required platform bridges (nohup+watchdog for background processes) with clear invariants (stale terminal turns), single source of truth (persisted session store), and test-only timing—no s...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR contains no NSWindow, NSPanel, NSWindowController, SwiftUI Window, or WindowGroup code. Changes are CLI-only: hook dispatch routing, shell command generation, and test coverage for hook behavior.
Cmux Source Artifacts ✅ Passed All changed paths are legitimate source/test files and configs: hand-written Swift implementations (CLI/CMUXCLI+CodexFireAndForgetHooks.swift, CLI/cmux.swift), Swift test suite (CLICodexHookTimeout...

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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-hook-timeouts

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: 1344b56020

ℹ️ 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/CMUXCLI+AgentHookDefinitions.swift Outdated
Comment on lines +393 to +394
if def.name == "codex" {
return codexFireAndForgetAgentHookShellCommand(command, for: def)

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 Codex lifecycle hook ordering

When CMUX_SURFACE_ID is set, this branch sends every Codex lifecycle hook (session-start, prompt-submit, and stop) through an independent background process. If Codex finishes a short turn while the earlier prompt-submit process is delayed starting or connecting to the socket, the later Stop process can run first; the prompt-submit handler in CLI/cmux.swift then still writes lifecycle/status back to Running after the Stop path has written Idle, leaving the sidebar/session store stale. The fire-and-forget wrapper should serialize per-session lifecycle commands or be limited to hooks where ordering cannot change state.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Jun 14, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Codex session-start and prompt-submit hooks now dispatch cmux in the background via nohup, spool stdin to a temp file, and immediately return {} to avoid Codex's 5-second hook timeout. Alongside the shell change, the CLI gains Codex-specific staleness guards in both hook handlers so that late or concurrent hook arrivals from the fire-and-forget background process do not resurrect finished turns or overwrite active-turn state.

  • CMUXCLI+CodexFireAndForgetHooks.swift generates the async wrapper (stdin spool → nohup background launch → 30 s watchdog → rm temp file); stop and feed hooks remain synchronous.
  • ClaudeHookSessionStore gains upsertCodexSessionStartIfFresh and upsertCodexPromptRunningIfFresh, each guarded by dedicated staleness predicates; clearCodexSessionStartTurnState wipes active/last-turn fields while preserving terminal-turn tombstones, and markPromptTurnsTerminal now also sets lastPromptTurnId.
  • agentPIDFromHookEnvironment is added to prefer CMUX_<AGENT>_PID env vars (forwarded by the fire-and-forget wrapper) over PPID-walking, and both handlers interleave multiple re-checks of staleness between their sequential side-effect steps.

Confidence Score: 4/5

Safe to merge with one correctness gap in the handler ordering worth addressing before it silently distorts analytics.

The restructured session-start and prompt-submit handlers call sendAgentFeedTelemetryUnlessSuppressed between the first and second staleness guards. Because fire-and-forget explicitly permits concurrent Codex hook arrivals, a concurrent prompt-submit can slip in between those two checks and cause feed telemetry to fire for an invocation that the code then rejects and silently drops. Everything else — the shell wrapper, the new store methods, PID attribution, terminal-turn tombstone preservation, and the installer replacement logic — is well-structured and covered by the new regression suite.

CLI/cmux.swift — specifically the ordering of sendAgentFeedTelemetryUnlessSuppressed relative to the multiple staleness re-checks in both the session-start and prompt-submit handlers.

Important Files Changed

Filename Overview
CLI/CMUXCLI+CodexFireAndForgetHooks.swift New 12-line file that generates the fire-and-forget shell command: spools stdin to a temp file, launches cmux via nohup with a 30-second kill watchdog, and immediately echoes {}. The sleep 30 watchdog was flagged in a prior review thread.
CLI/CMUXCLI+AgentHookDefinitions.swift Adds routing in hookCommandString so that session-start and prompt-submit for the "codex" agent delegate to the fire-and-forget template; all other agents and hook events use the existing synchronous path.
CLI/cmux.swift Adds upsertCodexSessionStartIfFresh, upsertCodexPromptRunningIfFresh, codexSessionStartIsStale, and clearCodexSessionStartTurnState to ClaudeHookSessionStore, plus a new agentPIDFromHookEnvironment helper. The session-start and prompt-submit handlers are restructured with multiple re-checks of staleness; sendAgentFeedTelemetryUnlessSuppressed is placed between the first and second stale checks, risking spurious telemetry emission for rejected events in a concurrent-arrival scenario.
cmuxTests/CLICodexHookTimeoutRegressionTests.swift New 726-line serialized test suite covering install replacement/deduplication, fire-and-forget fast-return vs slow cmux, stale prompt-submit rejection, and session-start freshness scenarios. All polling uses small 20 ms sleeps in test-only scaffolding.
cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift Renames and adjusts the testCodexPromptSubmitRefreshesLastTurnDiffBaseline test to match the new behavior where a terminal turn's baseline commit is preserved rather than replaced.
.github/swift-file-length-budget.tsv Budgets updated for cmux.swift (+403 lines), CMUXCLI+AgentHookDefinitions.swift (+6), reduced CLINotifyProcessIntegrationRegressionTests.swift (-14), and new CLICodexHookTimeoutRegressionTests.swift entry (726 lines).
cmux.xcodeproj/project.pbxproj Adds CMUXCLI+CodexFireAndForgetHooks.swift to the main CLI target and CLICodexHookTimeoutRegressionTests.swift to the test target.

Sequence Diagram

sequenceDiagram
    participant Codex
    participant HookShell as Hook Shell (fire-and-forget)
    participant TmpFile as Temp File ($TMPDIR)
    participant nohup as nohup bg process
    participant cmux as cmux CLI
    participant Store as ClaudeHookSessionStore
    participant Socket as cmux Socket

    Codex->>HookShell: "invoke hook (stdin = JSON payload)"
    HookShell->>TmpFile: "mktemp + cat stdin > $payload"
    HookShell->>nohup: "nohup sh -c runner $payload $cmux_cli ... &"
    HookShell-->>Codex: "echo {} (immediate return)"

    nohup->>cmux: "exec cmux hooks codex session-start < $payload"
    cmux->>Store: upsertCodexSessionStartIfFresh (PID check)
    alt session is stale (active turn or same-PID completed turn)
        Store-->>cmux: return false
        cmux-->>nohup: "print {} + return"
    else session is fresh
        Store-->>cmux: return true (clear lastPromptTurnId, keep terminalTurnIds)
        cmux->>Socket: sendAgentFeedTelemetry
        cmux->>Socket: publishAgentSurfaceResumeBinding
        cmux->>Socket: set_agent_lifecycle / set_status
    end
    nohup->>TmpFile: rm -f $payload
Loading

Reviews (11): Last reviewed commit: "Simplify Codex prompt submit result stat..." | Re-trigger Greptile

Comment thread CLI/CMUXCLI+AgentHookDefinitions.swift Outdated
Comment on lines +400 to +403
private static func codexFireAndForgetAgentHookShellCommand(_ command: String, for def: AgentHookDef) -> String {
let routedArguments = command.hasPrefix("cmux ") ? String(command.dropFirst("cmux ".count)) : command
return "cmux_cli=\"${CMUX_BUNDLED_CLI_PATH:-}\"; if [ -z \"$cmux_cli\" ] || [ ! -x \"$cmux_cli\" ]; then cmux_cli=\"$(command -v cmux 2>/dev/null || true)\"; fi; if [ -n \"$CMUX_SURFACE_ID\" ] && [ \"$\(def.disableEnvVar)\" != \"1\" ] && [ -n \"$cmux_cli\" ]; then payload=\"$(mktemp \"${TMPDIR:-/tmp}/cmux-codex-hook.XXXXXX\" 2>/dev/null || mktemp -t cmux-codex-hook 2>/dev/null)\" || { echo '{}'; exit 0; }; cat >\"$payload\" || true; if [ -n \"${CMUX_SOCKET_PATH:-}\" ]; then nohup sh -c 'payload=\"$1\"; shift; \"$@\" <\"$payload\" >/dev/null 2>&1 & child=\"$!\"; ( sleep 30; kill \"$child\" 2>/dev/null || true ) & watchdog=\"$!\"; wait \"$child\" 2>/dev/null || true; kill \"$watchdog\" 2>/dev/null || true; rm -f \"$payload\"' cmux-codex-hook \"$payload\" \"$cmux_cli\" --socket \"$CMUX_SOCKET_PATH\" \(routedArguments) >/dev/null 2>&1 & else nohup sh -c 'payload=\"$1\"; shift; \"$@\" <\"$payload\" >/dev/null 2>&1 & child=\"$!\"; ( sleep 30; kill \"$child\" 2>/dev/null || true ) & watchdog=\"$!\"; wait \"$child\" 2>/dev/null || true; kill \"$watchdog\" 2>/dev/null || true; rm -f \"$payload\"' cmux-codex-hook \"$payload\" \"$cmux_cli\" \(routedArguments) >/dev/null 2>&1 & fi; echo '{}'; else echo '{}'; fi"
}

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 sleep 30 watchdog is a wall-clock process-lifetime timer in production shell

The shell script embedded in codexFireAndForgetAgentHookShellCommand uses ( sleep 30; kill "$child" 2>/dev/null || true ) & to cap how long the background cmux invocation can run. This is a fixed-time wall-clock wait used for process lifecycle synchronization — exactly the pattern flagged by the runtime-no-hacky-sleeps rule. If cmux blocks on a socket for slightly over 30 s under load, the watchdog silently kills it without any signal from the owning subsystem, dropping the hook payload with no indication of failure.

On macOS, /usr/bin/timeout is not available by default, but consider perl -e 'alarm(30); exec @ARGV' -- or propagating a SIGTERM-based approach via Swift Process.terminate() rather than generating a shell-level watchdog. At minimum, the watchdog timeout should be a named constant, not a hardcoded literal embedded in a long string.

File Used: .github/review-bot-rules/runtime-no-hacky-sleeps.md (source)

@lawrencecchen
lawrencecchen force-pushed the feat-codex-hook-timeouts branch from 1344b56 to 46d4a90 Compare June 14, 2026 07:47

@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)
cmuxTests/CLICodexHookTimeoutRegressionTests.swift (1)

1-133: ⚠️ Potential issue | 🟠 Major

Reorganize into two commits to satisfy regression test policy.

This PR adds the regression test and implementation in a single commit. Per coding guidelines, regression tests require a two-commit structure:

  1. Commit 1: Add the failing test only (CI goes red, proving the test catches the bug)
  2. Commit 2: Add the fix (CI goes green)

This makes it visible in the GitHub PR UI that the test genuinely fails without the fix. Currently, both testCodexHookInstallReplacesSynchronousBundledHook() and testCodexInstalledHookReturnsBeforeSlowCmuxCommandFinishes() are added alongside their implementation, so CI cannot prove the tests are effective.

🤖 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/CLICodexHookTimeoutRegressionTests.swift` around lines 1 - 133,
Split your changes into two separate commits per regression test policy. In the
first commit, add only the two test methods
testCodexHookInstallReplacesSynchronousBundledHook() and
testCodexInstalledHookReturnsBeforeSlowCmuxCommandFinishes() along with their
supporting helper methods (codexHookTestEnvironment, codexHookCommands,
makeExecutableShellFile, waitForFile) to the test file. Do not include any
implementation changes in this commit. In the second commit, add only the
implementation code that makes these failing tests pass. This structure ensures
the PR demonstrates that the tests actually fail without the fix, validating
their effectiveness.

Source: Coding guidelines

🤖 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/CMUXCLI`+CodexFireAndForgetHooks.swift:
- Around line 1-6: The codexFireAndForgetAgentHookShellCommand method contains a
duplicated nohup sh -c block that appears in both the socket and non-socket
branches, with the only difference being the optional --socket flag. Refactor
this by building the cmux invocation arguments conditionally first (constructing
a string that includes --socket "$CMUX_SOCKET_PATH" only when CMUX_SOCKET_PATH
is set), then use a single nohup sh -c block with the conditionally-built
arguments instead of duplicating the entire dispatch block.

---

Outside diff comments:
In `@cmuxTests/CLICodexHookTimeoutRegressionTests.swift`:
- Around line 1-133: Split your changes into two separate commits per regression
test policy. In the first commit, add only the two test methods
testCodexHookInstallReplacesSynchronousBundledHook() and
testCodexInstalledHookReturnsBeforeSlowCmuxCommandFinishes() along with their
supporting helper methods (codexHookTestEnvironment, codexHookCommands,
makeExecutableShellFile, waitForFile) to the test file. Do not include any
implementation changes in this commit. In the second commit, add only the
implementation code that makes these failing tests pass. This structure ensures
the PR demonstrates that the tests actually fail without the fix, validating
their effectiveness.
🪄 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: 26f38cc8-cef5-4002-a9a9-1c02fa5e14e1

📥 Commits

Reviewing files that changed from the base of the PR and between 1344b56 and 46d4a90.

📒 Files selected for processing (5)
  • CLI/CMUXCLI+AgentHookDefinitions.swift
  • CLI/CMUXCLI+CodexFireAndForgetHooks.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/CLICodexHookTimeoutRegressionTests.swift
  • cmuxTests/CLIGenericHookPersistenceTests.swift
💤 Files with no reviewable changes (1)
  • cmuxTests/CLIGenericHookPersistenceTests.swift

Comment thread CLI/CMUXCLI+CodexFireAndForgetHooks.swift

@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: 2

🤖 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/CMUXCLI`+CodexFireAndForgetHooks.swift:
- Around line 2-4: The codexFireAndForgetAgentHookShellCommand method generates
a shell command that lacks a stable Codex-owned marker, preventing proper
identification and replacement of stale hooks in hooks.json. The shell-rich
wrapper with semicolons and ampersands is rejected by
isLegacyCmuxOwnedHookCommand, so the system can only match by exact string
equality, allowing older variants to coexist and cause duplicate Codex
executions. Add a distinctive Codex marker comment or identifier string (similar
to the existing cmux hooks codex or cmux codex-hook patterns) to the generated
shell command that makes it reliably identifiable for cleanup purposes,
independent of minor shell syntax variations.
- Line 4: The shell script in the returned string has a file validation check
that accepts directories as valid executables because [ -x "$cmux_cli" ] returns
true for searchable directories. This causes directory-valued
CMUX_BUNDLED_CLI_PATH to bypass the fallback to command -v cmux, resulting in
silent failures. Replace the check [ ! -x "$cmux_cli" ] with [ ! -f "$cmux_cli"
] in the condition to require a non-directory file instead, mirroring the
behavior of isExecutableFilePath(_:). This change should be made in the initial
validation condition within the shell script string where CMUX_BUNDLED_CLI_PATH
is checked.
🪄 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: 33d0368f-19f8-43a4-ba93-56e92452a8ff

📥 Commits

Reviewing files that changed from the base of the PR and between 46d4a90 and b936804.

📒 Files selected for processing (4)
  • CLI/CMUXCLI+AgentHookDefinitions.swift
  • CLI/CMUXCLI+CodexFireAndForgetHooks.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/CLICodexHookTimeoutRegressionTests.swift

Comment on lines +2 to +4
static func codexFireAndForgetAgentHookShellCommand(_ command: String, for def: AgentHookDef) -> String {
let routedArguments = command.hasPrefix("cmux ") ? String(command.dropFirst("cmux ".count)) : command
return "cmux_cli=\"${CMUX_BUNDLED_CLI_PATH:-}\"; if [ -z \"$cmux_cli\" ] || [ ! -x \"$cmux_cli\" ]; then cmux_cli=\"$(command -v cmux 2>/dev/null || true)\"; fi; if [ -n \"$CMUX_SURFACE_ID\" ] && [ \"$\(def.disableEnvVar)\" != \"1\" ] && [ -n \"$cmux_cli\" ]; then payload=\"$(mktemp \"${TMPDIR:-/tmp}/cmux-codex-hook.XXXXXX\" 2>/dev/null || mktemp -t cmux-codex-hook 2>/dev/null)\" || { echo '{}'; exit 0; }; cat >\"$payload\" || true; if [ -n \"${CMUX_SOCKET_PATH:-}\" ]; then nohup sh -c 'payload=\"$1\"; shift; \"$@\" <\"$payload\" >/dev/null 2>&1 & child=\"$!\"; ( sleep 30; kill \"$child\" 2>/dev/null || true ) & watchdog=\"$!\"; wait \"$child\" 2>/dev/null || true; kill \"$watchdog\" 2>/dev/null || true; rm -f \"$payload\"' cmux-codex-hook \"$payload\" \"$cmux_cli\" --socket \"$CMUX_SOCKET_PATH\" \(routedArguments) >/dev/null 2>&1 & else nohup sh -c 'payload=\"$1\"; shift; \"$@\" <\"$payload\" >/dev/null 2>&1 & child=\"$!\"; ( sleep 30; kill \"$child\" 2>/dev/null || true ) & watchdog=\"$!\"; wait \"$child\" 2>/dev/null || true; kill \"$watchdog\" 2>/dev/null || true; rm -f \"$payload\"' cmux-codex-hook \"$payload\" \"$cmux_cli\" \(routedArguments) >/dev/null 2>&1 & fi; echo '{}'; else echo '{}'; fi"

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

Add a stable Codex-owned marker to this wrapper.

This command no longer contains either existing Codex cleanup marker (cmux hooks codex / cmux codex-hook), and isLegacyCmuxOwnedHookCommand rejects shell-rich wrappers like this one because they contain ; and &. Reinstall/uninstall can therefore recognize the current async hook only by exact string equality, so an older fire-and-forget variant can survive in hooks.json beside the new one and reintroduce duplicate Codex executions.

♻️ Suggested fix
 extension CMUXCLI {
     static func codexFireAndForgetAgentHookShellCommand(_ command: String, for def: AgentHookDef) -> String {
         let routedArguments = command.hasPrefix("cmux ") ? String(command.dropFirst("cmux ".count)) : command
-        return "cmux_cli=\"${CMUX_BUNDLED_CLI_PATH:-}\"; if [ -z \"$cmux_cli\" ] || [ ! -x \"$cmux_cli\" ]; then cmux_cli=\"$(command -v cmux 2>/dev/null || true)\"; fi; if [ -n \"$CMUX_SURFACE_ID\" ] && [ \"$\(def.disableEnvVar)\" != \"1\" ] && [ -n \"$cmux_cli\" ]; then payload=\"$(mktemp \"${TMPDIR:-/tmp}/cmux-codex-hook.XXXXXX\" 2>/dev/null || mktemp -t cmux-codex-hook 2>/dev/null)\" || { echo '{}'; exit 0; }; cat >\"$payload\" || true; if [ -n \"${CMUX_SOCKET_PATH:-}\" ]; then nohup sh -c 'payload=\"$1\"; shift; \"$@\" <\"$payload\" >/dev/null 2>&1 & child=\"$!\"; ( sleep 30; kill \"$child\" 2>/dev/null || true ) & watchdog=\"$!\"; wait \"$child\" 2>/dev/null || true; kill \"$watchdog\" 2>/dev/null || true; rm -f \"$payload\"' cmux-codex-hook \"$payload\" \"$cmux_cli\" --socket \"$CMUX_SOCKET_PATH\" \(routedArguments) >/dev/null 2>&1 & else nohup sh -c 'payload=\"$1\"; shift; \"$@\" <\"$payload\" >/dev/null 2>&1 & child=\"$!\"; ( sleep 30; kill \"$child\" 2>/dev/null || true ) & watchdog=\"$!\"; wait \"$child\" 2>/dev/null || true; kill \"$watchdog\" 2>/dev/null || true; rm -f \"$payload\"' cmux-codex-hook \"$payload\" \"$cmux_cli\" \(routedArguments) >/dev/null 2>&1 & fi; echo '{}'; else echo '{}'; fi"
+        return ": cmux-codex-hook-v1; cmux_cli=\"${CMUX_BUNDLED_CLI_PATH:-}\"; if [ -z \"$cmux_cli\" ] || [ ! -x \"$cmux_cli\" ]; then cmux_cli=\"$(command -v cmux 2>/dev/null || true)\"; fi; if [ -n \"$CMUX_SURFACE_ID\" ] && [ \"$\(def.disableEnvVar)\" != \"1\" ] && [ -n \"$cmux_cli\" ]; then payload=\"$(mktemp \"${TMPDIR:-/tmp}/cmux-codex-hook.XXXXXX\" 2>/dev/null || mktemp -t cmux-codex-hook 2>/dev/null)\" || { echo '{}'; exit 0; }; cat >\"$payload\" || true; if [ -n \"${CMUX_SOCKET_PATH:-}\" ]; then nohup sh -c 'payload=\"$1\"; shift; \"$@\" <\"$payload\" >/dev/null 2>&1 & child=\"$!\"; ( sleep 30; kill \"$child\" 2>/dev/null || true ) & watchdog=\"$!\"; wait \"$child\" 2>/dev/null || true; kill \"$watchdog\" 2>/dev/null || true; rm -f \"$payload\"' cmux-codex-hook \"$payload\" \"$cmux_cli\" --socket \"$CMUX_SOCKET_PATH\" \(routedArguments) >/dev/null 2>&1 & else nohup sh -c 'payload=\"$1\"; shift; \"$@\" <\"$payload\" >/dev/null 2>&1 & child=\"$!\"; ( sleep 30; kill \"$child\" 2>/dev/null || true ) & watchdog=\"$!\"; wait \"$child\" 2>/dev/null || true; kill \"$watchdog\" 2>/dev/null || true; rm -f \"$payload\"' cmux-codex-hook \"$payload\" \"$cmux_cli\" \(routedArguments) >/dev/null 2>&1 & fi; echo '{}'; else echo '{}'; fi"
     }
 }
 if def.name == "codex" {
     markers.append("cmux codex-hook")
+    markers.append("cmux-codex-hook-v1")
 }

As per coding guidelines, stale-hook replacement here needs to stay correctness-driven against the authoritative hooks.json contents instead of depending on a byte-for-byte cached shell literal.

📝 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
static func codexFireAndForgetAgentHookShellCommand(_ command: String, for def: AgentHookDef) -> String {
let routedArguments = command.hasPrefix("cmux ") ? String(command.dropFirst("cmux ".count)) : command
return "cmux_cli=\"${CMUX_BUNDLED_CLI_PATH:-}\"; if [ -z \"$cmux_cli\" ] || [ ! -x \"$cmux_cli\" ]; then cmux_cli=\"$(command -v cmux 2>/dev/null || true)\"; fi; if [ -n \"$CMUX_SURFACE_ID\" ] && [ \"$\(def.disableEnvVar)\" != \"1\" ] && [ -n \"$cmux_cli\" ]; then payload=\"$(mktemp \"${TMPDIR:-/tmp}/cmux-codex-hook.XXXXXX\" 2>/dev/null || mktemp -t cmux-codex-hook 2>/dev/null)\" || { echo '{}'; exit 0; }; cat >\"$payload\" || true; if [ -n \"${CMUX_SOCKET_PATH:-}\" ]; then nohup sh -c 'payload=\"$1\"; shift; \"$@\" <\"$payload\" >/dev/null 2>&1 & child=\"$!\"; ( sleep 30; kill \"$child\" 2>/dev/null || true ) & watchdog=\"$!\"; wait \"$child\" 2>/dev/null || true; kill \"$watchdog\" 2>/dev/null || true; rm -f \"$payload\"' cmux-codex-hook \"$payload\" \"$cmux_cli\" --socket \"$CMUX_SOCKET_PATH\" \(routedArguments) >/dev/null 2>&1 & else nohup sh -c 'payload=\"$1\"; shift; \"$@\" <\"$payload\" >/dev/null 2>&1 & child=\"$!\"; ( sleep 30; kill \"$child\" 2>/dev/null || true ) & watchdog=\"$!\"; wait \"$child\" 2>/dev/null || true; kill \"$watchdog\" 2>/dev/null || true; rm -f \"$payload\"' cmux-codex-hook \"$payload\" \"$cmux_cli\" \(routedArguments) >/dev/null 2>&1 & fi; echo '{}'; else echo '{}'; fi"
static func codexFireAndForgetAgentHookShellCommand(_ command: String, for def: AgentHookDef) -> String {
let routedArguments = command.hasPrefix("cmux ") ? String(command.dropFirst("cmux ".count)) : command
return ": cmux-codex-hook-v1; cmux_cli=\"${CMUX_BUNDLED_CLI_PATH:-}\"; if [ -z \"$cmux_cli\" ] || [ ! -x \"$cmux_cli\" ]; then cmux_cli=\"$(command -v cmux 2>/dev/null || true)\"; fi; if [ -n \"$CMUX_SURFACE_ID\" ] && [ \"$\(def.disableEnvVar)\" != \"1\" ] && [ -n \"$cmux_cli\" ]; then payload=\"$(mktemp \"${TMPDIR:-/tmp}/cmux-codex-hook.XXXXXX\" 2>/dev/null || mktemp -t cmux-codex-hook 2>/dev/null)\" || { echo '{}'; exit 0; }; cat >\"$payload\" || true; if [ -n \"${CMUX_SOCKET_PATH:-}\" ]; then nohup sh -c 'payload=\"$1\"; shift; \"$@\" <\"$payload\" >/dev/null 2>&1 & child=\"$!\"; ( sleep 30; kill \"$child\" 2>/dev/null || true ) & watchdog=\"$!\"; wait \"$child\" 2>/dev/null || true; kill \"$watchdog\" 2>/dev/null || true; rm -f \"$payload\"' cmux-codex-hook \"$payload\" \"$cmux_cli\" --socket \"$CMUX_SOCKET_PATH\" \(routedArguments) >/dev/null 2>&1 & else nohup sh -c 'payload=\"$1\"; shift; \"$@\" <\"$payload\" >/dev/null 2>&1 & child=\"$!\"; ( sleep 30; kill \"$child\" 2>/dev/null || true ) & watchdog=\"$!\"; wait \"$child\" 2>/dev/null || true; kill \"$watchdog\" 2>/dev/null || true; rm -f \"$payload\"' cmux-codex-hook \"$payload\" \"$cmux_cli\" \(routedArguments) >/dev/null 2>&1 & fi; echo '{}'; else echo '{}'; fi"
}
🤖 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/CMUXCLI`+CodexFireAndForgetHooks.swift around lines 2 - 4, The
codexFireAndForgetAgentHookShellCommand method generates a shell command that
lacks a stable Codex-owned marker, preventing proper identification and
replacement of stale hooks in hooks.json. The shell-rich wrapper with semicolons
and ampersands is rejected by isLegacyCmuxOwnedHookCommand, so the system can
only match by exact string equality, allowing older variants to coexist and
cause duplicate Codex executions. Add a distinctive Codex marker comment or
identifier string (similar to the existing cmux hooks codex or cmux codex-hook
patterns) to the generated shell command that makes it reliably identifiable for
cleanup purposes, independent of minor shell syntax variations.

Source: Coding guidelines

Comment thread CLI/CMUXCLI+CodexFireAndForgetHooks.swift Outdated
@lawrencecchen
lawrencecchen force-pushed the feat-codex-hook-timeouts branch from b936804 to 771fe80 Compare June 14, 2026 08:36
Comment thread CLI/CMUXCLI+AgentHookDefinitions.swift Outdated
@lawrencecchen
lawrencecchen force-pushed the feat-codex-hook-timeouts branch from 771fe80 to f8a5be5 Compare June 14, 2026 08:41
Comment thread CLI/cmux.swift Outdated
@lawrencecchen
lawrencecchen force-pushed the feat-codex-hook-timeouts branch from f8a5be5 to eba7f03 Compare June 14, 2026 08:45

@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: 2

🤖 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/CLICodexHookTimeoutRegressionTests.swift`:
- Around line 402-412: The timeout handling for the process termination has a
race condition and can hang indefinitely on readDataToEndOfFile() if the process
ignores signals. Replace the asynchronous DispatchQueue.asyncAfter approach with
synchronous sequential waits: after process.terminate() is called, immediately
wait for exitSignal with a timeout; if still running after that timeout, call
process.interrupt() synchronously and wait again; if the process is still
running after that second wait, use SIGKILL as a last resort (via
process.terminate() with force flag or equivalent) to guarantee the process is
dead before attempting to read from the pipes with readDataToEndOfFile(). This
ensures the pipes will actually close and EOF will occur, preventing indefinite
blocking.
- Around line 329-332: The single Darwin.write call in the mock socket response
section does not handle partial writes, which can occur with stream sockets and
cause truncated JSON responses and flaky tests. Replace the single write
operation with a loop that checks the return value from Darwin.write, advances
the buffer pointer/offset by the actual number of bytes written on each
iteration, handles EINTR errors appropriately, and continues looping until all
bytes are successfully sent to the client file descriptor. Follow the
established pattern used elsewhere in the test codebase like
CLINotifyProcessTestSupport.swift for the correct implementation.
🪄 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: 63aa4392-5ecc-4c6b-8619-4f258db8dff6

📥 Commits

Reviewing files that changed from the base of the PR and between b936804 and 771fe80.

📒 Files selected for processing (5)
  • CLI/CMUXCLI+AgentHookDefinitions.swift
  • CLI/CMUXCLI+CodexFireAndForgetHooks.swift
  • CLI/cmux.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/CLICodexHookTimeoutRegressionTests.swift

Comment on lines +329 to +332
let response = mockSocketResponse(for: line, surfaceId: surfaceId) + "\n"
_ = response.withCString { ptr in
Darwin.write(clientFD, ptr, strlen(ptr))
}

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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -name "CLICodexHookTimeoutRegressionTests.swift" -type f

Repository: manaflow-ai/cmux

Length of output: 113


🏁 Script executed:

if [ -f "cmuxTests/CLICodexHookTimeoutRegressionTests.swift" ]; then
  wc -l cmuxTests/CLICodexHookTimeoutRegressionTests.swift
fi

Repository: manaflow-ai/cmux

Length of output: 115


🏁 Script executed:

if [ -f "cmuxTests/CLICodexHookTimeoutRegressionTests.swift" ]; then
  sed -n '320,350p' cmuxTests/CLICodexHookTimeoutRegressionTests.swift
fi

Repository: manaflow-ai/cmux

Length of output: 1327


🏁 Script executed:

rg -n "Darwin\.write" --type swift

Repository: manaflow-ai/cmux

Length of output: 4100


🏁 Script executed:

sed -n '335,370p' cmuxTests/CLICodexHookTimeoutRegressionTests.swift

Repository: manaflow-ai/cmux

Length of output: 1334


🏁 Script executed:

sed -n '1,100p' cmuxTests/CLICodexHookTimeoutRegressionTests.swift

Repository: manaflow-ai/cmux

Length of output: 5219


🏁 Script executed:

if [ -f "cmux.xcodeproj/project.pbxproj" ]; then
  rg -c "CLICodexHookTimeoutRegressionTests" cmux.xcodeproj/project.pbxproj
fi

Repository: manaflow-ai/cmux

Length of output: 62


🏁 Script executed:

sed -n '165,185p' cmuxTests/CLINotifyProcessTestSupport.swift

Repository: manaflow-ai/cmux

Length of output: 1008


🏁 Script executed:

sed -n '215,235p' cmuxTests/CLINotifyProcessTestSupport.swift

Repository: manaflow-ai/cmux

Length of output: 1024


Handle partial socket writes in mock responses.

Darwin.write on stream sockets can write fewer bytes than requested. Line 331 currently does a single write without checking the return value, which can truncate JSON responses and cause flaky tests. The established pattern throughout the test codebase (e.g., CLINotifyProcessTestSupport.swift) shows the correct approach: loop until all bytes are sent, advance the cursor by the bytes actually written, and handle EINTR.

Suggested fix
-                let response = mockSocketResponse(for: line, surfaceId: surfaceId) + "\n"
-                _ = response.withCString { ptr in
-                    Darwin.write(clientFD, ptr, strlen(ptr))
-                }
+                let responseData = Data((mockSocketResponse(for: line, surfaceId: surfaceId) + "\n").utf8)
+                responseData.withUnsafeBytes { rawBuffer in
+                    guard let base = rawBuffer.bindMemory(to: UInt8.self).baseAddress else { return }
+                    var remaining = rawBuffer.count
+                    var cursor = base
+                    while remaining > 0 {
+                        let written = Darwin.write(clientFD, cursor, remaining)
+                        if written > 0 {
+                            remaining -= written
+                            cursor = cursor.advanced(by: written)
+                        } else if written < 0 && errno == EINTR {
+                            continue
+                        } else {
+                            return
+                        }
+                    }
+                }
📝 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
let response = mockSocketResponse(for: line, surfaceId: surfaceId) + "\n"
_ = response.withCString { ptr in
Darwin.write(clientFD, ptr, strlen(ptr))
}
let responseData = Data((mockSocketResponse(for: line, surfaceId: surfaceId) + "\n").utf8)
responseData.withUnsafeBytes { rawBuffer in
guard let base = rawBuffer.bindMemory(to: UInt8.self).baseAddress else { return }
var remaining = rawBuffer.count
var cursor = base
while remaining > 0 {
let written = Darwin.write(clientFD, cursor, remaining)
if written > 0 {
remaining -= written
cursor = cursor.advanced(by: written)
} else if written < 0 && errno == EINTR {
continue
} else {
return
}
}
}
🤖 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/CLICodexHookTimeoutRegressionTests.swift` around lines 329 - 332,
The single Darwin.write call in the mock socket response section does not handle
partial writes, which can occur with stream sockets and cause truncated JSON
responses and flaky tests. Replace the single write operation with a loop that
checks the return value from Darwin.write, advances the buffer pointer/offset by
the actual number of bytes written on each iteration, handles EINTR errors
appropriately, and continues looping until all bytes are successfully sent to
the client file descriptor. Follow the established pattern used elsewhere in the
test codebase like CLINotifyProcessTestSupport.swift for the correct
implementation.

Comment thread cmuxTests/CLICodexHookTimeoutRegressionTests.swift
@lawrencecchen
lawrencecchen force-pushed the feat-codex-hook-timeouts branch from eba7f03 to c57aeb2 Compare June 14, 2026 08:50

@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: c57aeb20c5

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

extension CMUXCLI {
static func codexFireAndForgetAgentHookShellCommand(_ command: String, for def: AgentHookDef) -> String {
let routedArguments = command.hasPrefix("cmux ") ? String(command.dropFirst("cmux ".count)) : command
return "cmux_cli=\"${CMUX_BUNDLED_CLI_PATH:-}\"; if [ -z \"$cmux_cli\" ] || [ ! -x \"$cmux_cli\" ]; then cmux_cli=\"$(command -v cmux 2>/dev/null || true)\"; fi; if [ -n \"$CMUX_SURFACE_ID\" ] && [ \"$\(def.disableEnvVar)\" != \"1\" ] && [ -n \"$cmux_cli\" ]; then payload=\"$(mktemp \"${TMPDIR:-/tmp}/cmux-codex-hook.XXXXXX\" 2>/dev/null || mktemp -t cmux-codex-hook 2>/dev/null)\" || { echo '{}'; exit 0; }; cat >\"$payload\" || true; if [ -n \"${CMUX_SOCKET_PATH:-}\" ]; then nohup sh -c 'payload=\"$1\"; shift; \"$@\" <\"$payload\" >/dev/null 2>&1 & child=\"$!\"; ( sleep 30; kill \"$child\" 2>/dev/null || true ) & watchdog=\"$!\"; wait \"$child\" 2>/dev/null || true; kill \"$watchdog\" 2>/dev/null || true; rm -f \"$payload\"' cmux-codex-hook \"$payload\" \"$cmux_cli\" --socket \"$CMUX_SOCKET_PATH\" \(routedArguments) >/dev/null 2>&1 & else nohup sh -c 'payload=\"$1\"; shift; \"$@\" <\"$payload\" >/dev/null 2>&1 & child=\"$!\"; ( sleep 30; kill \"$child\" 2>/dev/null || true ) & watchdog=\"$!\"; wait \"$child\" 2>/dev/null || true; kill \"$watchdog\" 2>/dev/null || true; rm -f \"$payload\"' cmux-codex-hook \"$payload\" \"$cmux_cli\" \(routedArguments) >/dev/null 2>&1 & fi; echo '{}'; else echo '{}'; fi"

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 Preserve caller binding before detaching Codex hooks

When Codex runs in a cmux terminal where routing must be resolved from the caller process/TTY (for example a stale leaked CMUX_SURFACE_ID or an environment that lacks CMUX_WORKSPACE_ID), this detached nohup sh -c ... & invocation makes the real cmux hooks codex ... process run with stdin from the temp file and stdout/stderr on /dev/null, and it can be reparented before inferredAgentPID()/processBinding() runs. That bypasses the existing correctedDirectSurfaceId defense in runGenericAgentHook, so session-start/prompt-submit can fall back to the stale ambient surface or fail to resolve the workspace instead of targeting the Codex pane; capture/pass the caller tty or agent PID before detaching.

Useful? React with 👍 / 👎.

@lawrencecchen
lawrencecchen force-pushed the feat-codex-hook-timeouts branch from c57aeb2 to d72946b Compare June 14, 2026 08:59

@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: d72946b542

ℹ️ 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
turnId: input.turnId,
pid: pid,
launchCommand: launchCommand
)) ?? false

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 prompt UI when store writes fail

When upsertCodexPromptRunningIfFresh throws for reasons unrelated to staleness (for example the hook-state directory is missing/unwritable, so withLockedState cannot open the lock or save), try? converts that into false and the next branch returns as if this were a stale terminal turn. That skips the live socket updates that used to proceed even when store.upsert failed, so a valid Codex prompt-submit can fail to clear notifications or set the pane to Running solely because persistence is unavailable; distinguish store errors from the explicit stale-turn result.

Useful? React with 👍 / 👎.

@lawrencecchen
lawrencecchen force-pushed the feat-codex-hook-timeouts branch from d72946b to 718f7eb Compare June 14, 2026 09:04

@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: 718f7eb6b7

ℹ️ 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
if !sessionId.isEmpty {
let acceptedSessionStart: Bool
if def.name == "codex" {
acceptedSessionStart = (try? store.upsertCodexSessionStartIfFresh(

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 Let SessionStart continue when the store write fails

When the Codex hook state directory is missing or unwritable, upsertCodexSessionStartIfFresh throws while opening/saving the store, but try? turns that into false and this branch treats the valid SessionStart as stale. That returns before feed telemetry, resume binding, PID, lifecycle, and status updates; before this change the store write failure was ignored and the visible session still came up, so users with a bad ~/.cmuxterm state path lose Codex UI updates rather than only persistence.

Useful? React with 👍 / 👎.

@lawrencecchen
lawrencecchen force-pushed the feat-codex-hook-timeouts branch from 718f7eb to 4cf36e8 Compare June 14, 2026 09:09
Comment thread CLI/cmux.swift
Comment thread CLI/CMUXCLI+CodexFireAndForgetHooks.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: 4cf36e8652

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

let timedOut: Bool
}

@Test func codexHookInstallReplacesSynchronousBundledHook() throws {

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 Split regression tests into red/green commits

AGENTS.md in /workspace/cmux requires regression tests for a bug fix to use a two-commit structure: first add the failing test only, then add the fix. This commit adds CLICodexHookTimeoutRegressionTests together with the hook implementation changes, so CI can never demonstrate that these new regression tests fail without the fix. Please split the test-only change from the implementation change so the PR preserves the required red/green evidence.

Useful? React with 👍 / 👎.

@lawrencecchen
lawrencecchen force-pushed the feat-codex-hook-timeouts branch from 4cf36e8 to 7f774b4 Compare June 14, 2026 09:15
# Conflicts:
#	.github/swift-file-length-budget.tsv
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: da98f613cf

ℹ️ 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 +1147 to +1148
if max(record.activePromptDepth ?? 0, record.activePromptTurnIds?.count ?? 0) > 0 {
return 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.

P2 Badge Allow new Codex PIDs to recover active sessions

When a Codex process is killed or crashes while a turn is active, the store can retain activePromptDepth/activePromptTurnIds. If the user resumes the same Codex session in a new process, the SessionStart hook arrives with a different PID, but this guard rejects it before comparing PIDs, so the hook returns without clearing the stale turn state or recording the new PID/status. This leaves the pane stuck on the old running session; only a same-process late SessionStart should be treated as stale here.

Useful? React with 👍 / 👎.

@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 68a6d3e. Configure here.

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: 6c8aef3bc8

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

# Format: max_lines<TAB>relative path
# Reduce counts as files shrink. CI fails if tracked files exceed this budget.
33454 CLI/cmux.swift
33857 CLI/cmux.swift

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 Extract Codex hooks instead of raising the budget

The repo's .github/review-bot-rules/swift-file-package-boundaries.md says to report an existing production Swift file over 800 lines gaining more than 250 lines unless the change extracts responsibility or shrinks the oversized file. This commit adds hundreds of lines of Codex hook dispatch, persistence freshness, and socket/UI restore logic to the already 33k-line CLI/cmux.swift and accepts that growth by raising the budget here, so the smallest fix is to move the Codex hook freshness/fire-and-forget support behind a focused file or package boundary instead of increasing the monolith's allowance.

Useful? React with 👍 / 👎.

@lawrencecchen
lawrencecchen merged commit 249a1d4 into main Jun 14, 2026
21 of 23 checks passed
@lawrencecchen
lawrencecchen deleted the feat-codex-hook-timeouts branch June 14, 2026 10:24
hhsw2015 pushed a commit to hhsw2015/cmux that referenced this pull request Jun 14, 2026
* Make Codex hooks fire-and-forget

* Preserve Codex prompt baseline refreshes

* Preserve Codex turn tombstones

* Harden Codex stale hook ordering

* Drop stale Codex prompt baseline writes

* Simplify Codex prompt submit result state
@lawrencecchen
lawrencecchen restored the feat-codex-hook-timeouts branch July 18, 2026 10:24

This branch was successfully deployed

1 active deployment
Preview – cmux — 6c8aef3b Deployed Jun 14, 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