Skip to content

Scope Codex hooks per launch and serialize delivery - #8243

Closed
lawrencecchen wants to merge 2 commits into
mainfrom
issue-8230-codex-hook-scope
Closed

lawrencecchen wants to merge 2 commits into
mainfrom
issue-8230-codex-hook-scope

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jul 16, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #8230

What changed

  • cmux hooks setup leaves Codex to the per-launch wrapper by default.
  • The wrapper migrates unmarked cmux-owned hooks out of ~/.codex/hooks.json before Codex reads it, while preserving user-owned hooks.
  • Explicit cmux hooks codex install records persistent mode for custom launchers; the wrapper then enables hooks without injecting duplicates.
  • Background hook deliveries use a per-Codex-PID lockf queue, preserving event order while hook commands return {} immediately.

Root cause

Persistent global registration and wrapper injection both owned the same Codex events. The global scripts ran in standalone sessions even when CMUX_SURFACE_ID was absent, and wrapped sessions could load both producers. Transport was already fire-and-forget, but separate background processes had no ordering primitive.

Verification

  • First commit adds four behavioral CLI regressions; second commit fixes them.
  • Tagged macOS app and bundled CLI built on Blacksmith: https://github.com/manaflow-ai/cmux/actions/runs/29471644334
  • Isolated CLI verification: explicit mode installed 10 hooks and emitted only --enable hooks; migration removed all 10 cmux hooks and emitted one 15-argument scoped hook set.
  • 100 wrapper hook invocations returned in 2.52s total, 25.2ms mean, against a disposable receiver.
  • swiftc -parse, bash -n, and git diff --check pass.
  • No user-facing strings changed, so localization catalogs are unchanged.

Design

This is a process-ownership fix, not a language-runtime optimization. Rust would still pay process launch and IPC costs. The existing shell producer now returns quickly, and the OS file lock supplies a bounded FIFO queue without another daemon.


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


Summary by cubic

Scope Codex hooks to each launch and queue background deliveries per Codex process to keep events ordered and avoid blocking. This removes duplicate producers and stabilizes hook execution.

  • New Features

    • cmux hooks setup now leaves Codex to the per-launch wrapper by default; the wrapper migrates cmux-owned entries out of ~/.codex/hooks.json and keeps user hooks.
    • Explicit installs via cmux hooks codex install persist hooks and create a .cmux-persistent-hooks-opt-in marker; wrapper then emits only --enable hooks without injecting duplicates.
    • Background delivery uses a per-Codex-PID lockf queue so hooks return {} immediately while events run in FIFO order.
    • Quiet uninstall path added to support seamless migration during wrapper injection.
  • Migration

    • No action needed for default Codex launches; migration runs automatically.
    • For custom launchers that want persistent hooks, run: cmux hooks codex install.

Written for commit 98464de. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added persistent Codex hook support with opt-in tracking.
    • Added serialized background hook delivery to preserve execution order.
    • Improved Codex hook setup and migration behavior.
  • Bug Fixes

    • Prevented duplicate or legacy hooks from being retained.
    • Improved quiet uninstallation behavior and deferred Codex setup handling.
    • Ensured pre- and post-tool hooks execute in the correct sequence.
  • Tests

    • Added coverage for persistent hook migration, setup, delivery ordering, and timeout-related behavior.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Codex hook injection now tracks persistent-hook opt-in state, removes legacy cmux hooks when appropriate, and defers to explicit persistent hooks. Fire-and-forget delivery is serialized with per-agent locks, with regression tests covering migration, setup, and event ordering.

Changes

Codex hook lifecycle and delivery

Layer / File(s) Summary
Persistent hook ownership lifecycle
CLI/CMUXCLI+CodexFireAndForgetHooks.swift, CLI/cmux.swift
Codex hook injection records opt-in state, detects cmux-owned persistent hooks, cleans up legacy installations, supports quiet uninstallation, and skips Codex during unfiltered uninstall operations.
Serialized fire-and-forget delivery
CLI/CMUXCLI+CodexFireAndForgetHooks.swift, Resources/bin/cmux-codex-wrapper
Generated hook runners use a per-agent lockf queue lock, while wrapper documentation describes locked background delivery and immediate {} responses.
Codex hook regression coverage
cmuxTests/CLICodexHookTimeoutRegressionTests.swift
Tests cover legacy migration, explicit persistent-hook preservation, deferred setup, and ordered PreToolUse/PostToolUse delivery.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CMUXCLI
  participant hooks.json
  participant Codex
  participant HookRunner
  participant cmux
  CMUXCLI->>hooks.json: inspect persistent hook ownership
  CMUXCLI->>Codex: emit enable-only or injected hook arguments
  Codex->>HookRunner: execute hook command
  HookRunner->>HookRunner: acquire per-agent queue lock
  HookRunner->>cmux: deliver captured event payload
  HookRunner-->>Codex: return {}
Loading

Possibly related PRs

  • manaflow-ai/cmux#4225: Expands legacy cmux-owned Codex hook command detection used by persistent-hook cleanup.
  • manaflow-ai/cmux#7410: Also changes Codex fire-and-forget hook behavior and delivery-order testing.

Suggested reviewers: azooz2003-bit


Important

Pre-merge checks failed

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

❌ Failed checks (4 errors)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error The Swift diff adds production hook runtime with /usr/bin/lockf -k -t 30, sleep 30, and wait, which the rule treats as blocked synchronization. Replace the shell lock/sleep queue with explicit async signaling or actor-owned ordering; avoid lockf, waits, and watchdog sleeps in shipped runtime code.
Cmux No Hacky Sleeps ❌ Error The new production hook runner adds lockf -k -t 30, a fixed 30s wall-clock wait to serialize delivery, which this rule forbids. Replace the timeout-based queue with an event-driven handoff: enqueue and exit, then serialize in a dedicated worker without sleep/-t waits.
Cmux Swift Package Boundaries ❌ Error The diff adds reusable Codex hook ownership/queue logic in the CLI target; it’s not UI/AppKit/Ghostty glue and belongs behind a SwiftPM boundary. Move the Codex hook-state, JSON inspection, and fire-and-forget queue builders into a small package target (likely CMUXAgentLaunch or a new CMUXCodexHooks), leaving CLI orchestration only.
Cmux Architecture Rethink ❌ Error The diff adds a marker-file side channel for persistent-hook opt-in and a per-PID lockf queue with sleep/watchdog, both forbidden by the rethink rule. Refactor to one authoritative hook-owner state and remove lock/sleep-based queuing; preserve ordering without adding mutable side channels.
✅ Passed checks (21 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: scoping Codex hooks per launch and serializing delivery.
Description check ✅ Passed The description covers summary, root cause, verification, and design, but it omits some template sections like Demo Video, Review Trigger, and Checklist.
Linked Issues check ✅ Passed The changes address standalone hook leakage, duplicate producers, ordered fire-and-forget delivery, and regression coverage for both launch paths.
Out of Scope Changes check ✅ Passed The edits stay focused on Codex hook scope, migration, delivery ordering, and tests, with only matching comment/doc updates.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Cmux Swift Actor Isolation ✅ Passed The patch only adds CLI shell-string helpers and tests; CMUXCLI stays a plain struct, with no new @MainActor, Sendable, or UI-bound isolation mistakes.
Cmux Browser Automation Off-Main ✅ Passed Diff only touches Codex hook install/wrapper files; no browser.* commands or socket-worker/mainActor routing changes appear in the changed hunks.
Cmux Expensive Synchronous Load ✅ Passed Added Codex hook-state checks only read small ~/.codex/hooks.json on CLI launch/install paths; no new MainActor/UI or agent-history loader was introduced.
Cmux Cache Substitution Correctness ✅ Passed The opt-in marker is freshness-checked against hooks.json on use, and stale markers are removed; no authoritative persistence read was replaced by an unchecked cache.
Cmux Algorithmic Complexity ✅ Passed The new scans are over fixed-size Codex event lists (3 + 7 entries) or a single hooks.json tree walk; no nested rescans or hot-path sorting/filtering on scalable records.
Cmux Swift Concurrency ✅ Passed The Swift diff adds only synchronous file/JSON logic and shell-lock orchestration; no new DispatchQueue, Task, Combine, or completion-handler patterns appear.
Cmux Swift @Concurrent ✅ Passed The diff adds no @concurrent/nonisolated async declarations or UI-isolated async helpers; the touched Swift code remains synchronous shell/config logic.
Cmux Swiftpm Lockfiles ✅ Passed No SwiftPM/Xcode/.gitignore/workflow/dependency files changed; diff only touches Swift sources and wrapper docs, so Package.resolved policy isn't implicated.
Cmux Swift Logging ✅ Passed No added/expanded diagnostic logging in production Swift; changed stdout writes are intended CLI output, and the only print changes suppress existing status messages.
Cmux User-Facing Error Privacy ✅ Passed The patch only gates existing uninstall prints behind quiet and changes comments/tests; no new user-facing errors or outputs expose banned details.
Cmux Full Internationalization ✅ Passed Only developer-facing comments, test code, and behavior changes were touched; no user-facing text or locale/catalog files were added or changed.
Cmux Swiftui State Layout ✅ Passed Only CLI hook/script logic changed; no SwiftUI views or state/layout anti-patterns were added.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR only changes Codex hook plumbing/tests; no touched Swift code adds or alters NSWindow/WindowGroup ownership or close-shortcut routing.
Cmux Source Artifacts ✅ Passed Changed paths are source/tests/scripts only; no logs, caches, temp dirs, build output, or scratch artifacts appear in diff.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No touched Swift file is under **/Sources/**, and the patch adds no test/debug seam; the widened uninstallAgentHooks is used by production code.
Cmux No Ambient Global State ✅ Passed No new file-scope API, mutable global, or singleton was added; the new Codex helpers are scoped on existing CMUXCLI, with only a static constant and instance/private helpers.
✨ 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 issue-8230-codex-hook-scope

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.

@greptile-apps

greptile-apps Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a process-ownership conflict where both the global persistent Codex hooks and the per-launch wrapper injection could simultaneously own the same Codex hook events. It introduces a per-session lockf queue to serialize background hook deliveries in order, migrates unmarked cmux-owned hooks out of ~/.codex/hooks.json at wrapper launch time (preserving user hooks), and adds a .cmux-persistent-hooks-opt-in marker file so explicit cmux hooks codex install mode still works for custom launchers.

  • cmux hooks setup now skips Codex by default, leaving all hook injection to the per-launch wrapper; cmux hooks codex install continues to write global persistent hooks and records an opt-in marker so the wrapper defers to them instead of migrating.
  • The fire-and-forget runner gains a queue_lock argument: each background delivery acquires a per-Codex-PID lockf lock before running the real cmux call, serializing event order while every hook still returns {} to Codex instantly.
  • Four new regression tests cover migration, opt-in deferral, default-setup exclusion, and delivery-order serialization.

Confidence Score: 4/5

Safe to merge; the migration logic is self-limiting and atomic, the serialization mechanism is correct for the benchmarked workload, and tests cover the four stated behavioral regressions.

Two non-blocking concerns exist in the new runner script. First, the 30-second watchdog for each background delivery starts counting from when the nohup shell spawns, not from when the lock is actually acquired — so a command that waits near 30s for a contended lock gets almost no execution time before being killed and having its payload silently deleted. Second, the non-numeric PID sanitization does not match an empty string, meaning a pathologically empty agent_pid falls through to a shared lock file. Neither issue surfaces under normal conditions given the 25ms benchmarks.

CLI/CMUXCLI+CodexFireAndForgetHooks.swift — specifically the runner shell string on line 203 (watchdog timing) and the PID sanitization case on line 208.

Important Files Changed

Filename Overview
CLI/CMUXCLI+CodexFireAndForgetHooks.swift Core hook dispatch logic: adds per-session lockf serialization, migration of legacy global hooks, and a persistent-mode marker file. Two latent issues: the 30s watchdog shares its budget with lock acquisition time, and the empty-string PID case bypasses the non-numeric guard.
CLI/cmux.swift Widens uninstallAgentHooks from private to internal (needed for cross-extension call) and adds quiet mode; skips codex during global hooks setup; records/clears persistent opt-in marker on explicit install/uninstall. Changes are minimal and correct.
Resources/bin/cmux-codex-wrapper Comment-only update reflecting the new per-session file-lock queue semantics. No logic changes; passes bash -n cleanly.
cmuxTests/CLICodexHookTimeoutRegressionTests.swift Adds four new behavioral regression tests: migration of legacy hooks, persistent opt-in deferral, default setup exclusion, and serialized delivery ordering. Tests are test-only scaffolding in the test target; no production seam concerns.

Reviews (1): Last reviewed commit: "fix: isolate Codex hook ownership" | Re-trigger Greptile

static func codexFireAndForgetAgentHookShellCommand(_ command: String, for def: AgentHookDef) -> String {
let routedArguments = command.hasPrefix("cmux ") ? String(command.dropFirst("cmux ".count)) : command
let runner = "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\""
let runner = "payload=\"$1\"; queue_lock=\"$2\"; shift 2; /usr/bin/lockf -k -t 30 \"$queue_lock\" \"$@\" <\"$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\""

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 Watchdog budget shared between lock wait and command execution

The 30-second sleep 30; kill $child watchdog starts counting from when the nohup shell spawns, not from when lockf actually acquires the lock and execs the command. Because lockf -k -t 30 may consume up to 30s waiting for the lock, a delivery that arrives late in a contention window can get nearly zero execution time before the watchdog fires — the payload is then silently deleted via rm -f "$payload". The old code gave each command the full 30s budget. Under the benchmarked 25ms mean this won't bite, but a slow cmux call holding the lock for several seconds causes the next delivery's watchdog to fire almost immediately after it acquires the lock.

"if [ -z \"$cmux_cli\" ] || [ ! -x \"$cmux_cli\" ]; then cmux_cli=\"$(command -v cmux 2>/dev/null || true)\"; fi",
"agent_pid=\"${CMUX_CODEX_PID:-${PPID:-}}\"",
"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 CMUX_CODEX_PID=\"$agent_pid\" nohup sh -c '\(runner)' cmux-codex-hook \"$payload\" \"$cmux_cli\" --socket \"$CMUX_SOCKET_PATH\" \(routedArguments) >/dev/null 2>&1 & else CMUX_CODEX_PID=\"$agent_pid\" nohup sh -c '\(runner)' cmux-codex-hook \"$payload\" \"$cmux_cli\" \(routedArguments) >/dev/null 2>&1 & fi; echo '{}'; else echo '{}'; fi",
"case \"$agent_pid\" in *[!0-9]*) agent_pid=\"${PPID:-$$}\" ;; esac",

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 An empty agent_pid does not match *[!0-9]* because the bracket expression requires at least one character. Adding the empty-string arm closes this gap and ensures all non-positive-integer PIDs fall through to the PPID/$$ fallback.

Suggested change
"case \"$agent_pid\" in *[!0-9]*) agent_pid=\"${PPID:-$$}\" ;; esac",
"case \"$agent_pid\" in \"\" | *[!0-9]*) agent_pid=\"${PPID:-$$}\" ;; esac",

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!

@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 86-93: Make persistent ownership authoritative in the hooks
configuration managed by prepareCodexWrapperHookOwnership and the related
installation/removal flow, rather than using the separate opt-in marker file.
Update installation and cleanup so the owned hook entries and ownership state
are written or removed through one atomic config transformation, ensuring the
wrapper cannot classify newly installed persistent hooks as legacy between
separate writes. Remove the parallel marker-based authority while preserving
explicit persistent installation and uninstallation behavior.
- Around line 203-210: The fire-and-forget runner in the generated `runner`
shell command lets queue-lock contention consume the entire 30-second watchdog
budget and then deletes the payload without execution. Update the runner and its
invocation in the hook construction to separate lock acquisition from the
execution watchdog, or otherwise persist and retry payloads that cannot acquire
`queue_lock`; ensure queued deliveries are not killed or removed solely because
waiting exceeded 30 seconds.
🪄 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: b163bc92-963c-481d-a867-4e84f888cd42

📥 Commits

Reviewing files that changed from the base of the PR and between 760c6f7 and 98464de.

📒 Files selected for processing (4)
  • CLI/CMUXCLI+CodexFireAndForgetHooks.swift
  • CLI/cmux.swift
  • Resources/bin/cmux-codex-wrapper
  • cmuxTests/CLICodexHookTimeoutRegressionTests.swift

Comment on lines +86 to +93
private func prepareCodexWrapperHookOwnership(_ def: AgentHookDef) throws -> Bool {
if Self.codexPersistentHooksAreOptedIn(for: def) {
return true
}
Self.removeCodexPersistentHookOptIn(for: def)
if Self.codexPersistentHooksAreInstalled(for: def) {
try uninstallAgentHooks(def, quiet: 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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make persistent ownership one atomic source of truth.

The marker and hooks.json are updated separately. If the wrapper runs after installAgentHooks writes the hooks but before recordCodexPersistentHookOptIn, Lines 91-92 classify those newly installed hooks as legacy and uninstall them. Explicit persistent installation can therefore return successfully while leaving no persistent hooks.

Store the opt-in marker with the owned hook entries in the same atomic config transformation, rather than maintaining a second file-backed authority.

As per coding guidelines, correctness-critical lifecycle state must use one authoritative source, without mutable side channels representing the same state. <coding_guidelines> <path_instructions>

Also applies to: 106-136

🤖 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 86 - 93, Make
persistent ownership authoritative in the hooks configuration managed by
prepareCodexWrapperHookOwnership and the related installation/removal flow,
rather than using the separate opt-in marker file. Update installation and
cleanup so the owned hook entries and ownership state are written or removed
through one atomic config transformation, ensuring the wrapper cannot classify
newly installed persistent hooks as legacy between separate writes. Remove the
parallel marker-based authority while preserving explicit persistent
installation and uninstallation behavior.

Sources: Coding guidelines, Path instructions

Comment on lines +203 to +210
let runner = "payload=\"$1\"; queue_lock=\"$2\"; shift 2; /usr/bin/lockf -k -t 30 \"$queue_lock\" \"$@\" <\"$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\""
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",
"agent_pid=\"${CMUX_CODEX_PID:-${PPID:-}}\"",
"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 CMUX_CODEX_PID=\"$agent_pid\" nohup sh -c '\(runner)' cmux-codex-hook \"$payload\" \"$cmux_cli\" --socket \"$CMUX_SOCKET_PATH\" \(routedArguments) >/dev/null 2>&1 & else CMUX_CODEX_PID=\"$agent_pid\" nohup sh -c '\(runner)' cmux-codex-hook \"$payload\" \"$cmux_cli\" \(routedArguments) >/dev/null 2>&1 & fi; echo '{}'; else echo '{}'; fi",
"case \"$agent_pid\" in *[!0-9]*) agent_pid=\"${PPID:-$$}\" ;; esac",
"queue_lock=\"${TMPDIR:-/tmp}/cmux-codex-hook-${agent_pid}.lock\"",
"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 CMUX_CODEX_PID=\"$agent_pid\" nohup sh -c '\(runner)' cmux-codex-hook \"$payload\" \"$queue_lock\" \"$cmux_cli\" --socket \"$CMUX_SOCKET_PATH\" \(routedArguments) >/dev/null 2>&1 & else CMUX_CODEX_PID=\"$agent_pid\" nohup sh -c '\(runner)' cmux-codex-hook \"$payload\" \"$queue_lock\" \"$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.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

set -euo pipefail

# Inspect the relevant file and nearby context
FILE='CLI/CMUXCLI+CodexFireAndForgetHooks.swift'
wc -l "$FILE"
sed -n '160,245p' "$FILE"

# Look for any other uses of lockf / watchdog / payload deletion in the repo
rg -n --hidden --no-ignore-vcs 'lockf -k|-t 30|watchdog|rm -f "\$payload"|cmux-codex-hook' .

Repository: manaflow-ai/cmux

Length of output: 50374


🌐 Web query:

macOS lockf -t timeout semantics command skipped if lock not acquired man page

💡 Result:

On macOS, the lockf system call does not support timeout semantics [1]. The standard lockf function, as defined by POSIX and implemented on macOS, provides advisory record locking that either blocks indefinitely (F_LOCK) or returns immediately with an error (F_TLOCK) if the lock cannot be acquired [2][3]. It is important to distinguish between the lockf function (a library/system call) and the lockf utility (a command-line tool). The confusion regarding "timeout semantics" and "skipping command if lock not acquired" likely stems from the lockf utility available on other platforms (such as FreeBSD or certain Linux distributions), which often includes a -t or -w flag to handle timeouts and conditional command execution [4]. macOS does not provide a built-in lockf command-line utility with these features [1]. Developers requiring timeout functionality for file locking on macOS must typically implement it manually, for example, by using the alarm function in conjunction with standard blocking locks, or by using more modern file locking mechanisms like flock or fcntl with appropriate application-level logic [2][3][5].

Citations:


🏁 Script executed:

set -euo pipefail

# Try to read the local man page if available; otherwise fall back to help text.
( man lockf 2>/dev/null | col -b | sed -n '1,220p' ) || ( /usr/bin/lockf -h 2>&1 || true )

Repository: manaflow-ai/cmux

Length of output: 247


🏁 Script executed:

set -euo pipefail
( man lockf 2>/dev/null | col -b | sed -n '1,220p' ) || ( /usr/bin/lockf -h 2>&1 || true )

Repository: manaflow-ai/cmux

Length of output: 247


🏁 Script executed:

set -euo pipefail
FILE='cmuxTests/CLICodexHookTimeoutRegressionTests.swift'
wc -l "$FILE"
sed -n '1,340p' "$FILE"

Repository: manaflow-ai/cmux

Length of output: 18275


🏁 Script executed:

set -euo pipefail
rg -n 'lockf|queue_lock|timed out|timeout|watchdog' cmuxTests/CLICodexHookTimeoutRegressionTests.swift CLI/CMUXCLI+CodexFireAndForgetHooks.swift
sed -n '340,520p' cmuxTests/CLICodexHookTimeoutRegressionTests.swift

Repository: manaflow-ai/cmux

Length of output: 14147


🏁 Script executed:

set -euo pipefail
sed -n '1,260p' CLI/CMUXCLI+CodexFireAndForgetHooks.swift

Repository: manaflow-ai/cmux

Length of output: 11775


🏁 Script executed:

set -euo pipefail
rg -n 'cmux-codex-hook|queue_lock|lockf -k -t 30|hooks codex prompt-submit|hooks codex session-start|hooks codex stop' CLI Packages cmuxTests

Repository: manaflow-ai/cmux

Length of output: 13068


Do not let queue wait consume the 30-second delivery budget. CLI/CMUXCLI+CodexFireAndForgetHooks.swift:203-210

lockf starts before the payload command runs, and the watchdog starts at the same time. Under contention, a delivery can spend most of its budget waiting on queue_lock, get killed before execution, and still have "$payload" deleted afterward. Split queueing from execution or persist/retry the payload instead of dropping it.

🤖 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 203 - 210, The
fire-and-forget runner in the generated `runner` shell command lets queue-lock
contention consume the entire 30-second watchdog budget and then deletes the
payload without execution. Update the runner and its invocation in the hook
construction to separate lock acquisition from the execution watchdog, or
otherwise persist and retry payloads that cannot acquire `queue_lock`; ensure
queued deliveries are not killed or removed solely because waiting exceeded 30
seconds.

Source: Coding guidelines

@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Codex shows cmux hook activity outside cmux and can load duplicate feed hooks

2 participants