Skip to content

Run agent lifecycle hooks in background - #5774

Closed
lawrencecchen wants to merge 1 commit into
mainfrom
feat-background-agent-hooks
Closed

lawrencecchen wants to merge 1 commit into
mainfrom
feat-background-agent-hooks

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 10, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • install cmux lifecycle hook commands as fire-and-forget background dispatchers for all generic agent hook integrations
  • keep Feed bridge hooks foreground so approval responses and deny exit codes still propagate
  • retain old synchronous Codex hook trust hashes so setup can replace previously trusted commands

Verification

  • xcodebuild -project cmux.xcodeproj -scheme cmux-cli -derivedDataPath /tmp/cmux-bghook-cli build
  • installed Codex hooks with the tagged CLI into a temp CODEX_HOME; SessionStart/UserPromptSubmit/Stop all contained cmux-agent-hook-background-v1 and nohup sh -c
  • executed the installed UserPromptSubmit command against a fake slow cmux handler: hook stdout {}, returned in 32ms, handler completed later with the payload
  • ./scripts/reload-cloud.sh --tag bghook
  • launched tagged bghook app, created a Codex workspace through scripts/cmux-debug-cli.sh, and confirmed BGHOOK_DONE_AUTH with no hook timeout/exited banners

Notes

  • Local cmux-unit build is blocked in this worktree by missing GhosttyKit.xcframework; CI should compile and run the full test target.

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


Note

Medium Risk
Changes how every integrated agent invokes cmux on lifecycle events (session, prompts, stop); wrong background dispatch could drop telemetry or restore updates, while feed paths remain blocking by design.

Overview
Lifecycle agent hooks now install as fire-and-forget shell wrappers (cmux-agent-hook-background-v1): they write the hook JSON to a temp file, run the real cmux hooks <agent> <event> via nohup, immediately print {}, and log to cmux-agent-hooks. A worker caps detached runs at ~30s.

Feed bridge hooks are unchanged—still synchronous so approvals and deny exit codes (e.g. Kiro exit 2) reach the agent.

For Codex trust hashes, setup also registers the previous synchronous lifecycle command strings so reinstall can replace already-trusted hook commands.

Tests cover background vs foreground split and that lifecycle hooks return before a slow handler finishes; docs describe the behavior.

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


Summary by cubic

Run agent lifecycle hooks in the background so agents don’t block, while keeping Feed bridge hooks in the foreground so approval/deny behavior still works.

  • New Features
    • Lifecycle hooks now fire-and-forget: copy payload to temp, spawn cmux hooks <agent> <event> with nohup, return {} immediately.
    • Feed bridge hooks remain foreground to propagate approvals and denial exit codes; timeouts preserved.
    • Preserve trust for existing installs by inserting hashes for legacy synchronous commands; added tests and docs for the new behavior.

Written for commit 252da29. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

Release Notes

  • New Features

    • Lifecycle hooks now execute as background processes, returning immediately to avoid blocking agent operations
    • Feed bridge hooks remain foreground with extended timeouts to support approval decisions
  • Tests

    • Added validation for background hook dispatch behavior and execution timing
  • Documentation

    • Clarified lifecycle hook execution model and feed hook behavior

@vercel

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

@coderabbitai

coderabbitai Bot commented Jun 10, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR converts lifecycle hooks to background fire-and-forget execution while preserving legacy synchronous behavior. A new backgroundAgentHookShellCommand helper stages payloads and wraps commands in nohup workers with bounded process lifetimes and logging; hookCommandString now delegates to this helper. Legacy synchronous registration is preserved via legacySynchronousHookCommandString. The changes are wired into hash registration, validated with dispatch and timing tests, and documented.

Changes

Agent Hook Background Execution

Layer / File(s) Summary
Background hook execution helpers
CLI/CMUXCLI+AgentHookDefinitions.swift
hookCommandString now delegates to backgroundAgentHookShellCommand, which stages hook payloads in temp files, runs commands via embedded sh scripts within nohup workers, manages child process lifecycle with ~30-second wait/kill loops, and logs to cmux-agent-hooks/<logSlug>.log. New legacySynchronousHookCommandString preserves prior synchronous agentHookShellCommand behavior.
Hook command registration wiring
CLI/cmux.swift
An additional insertHashes call registers the legacy synchronous hook command with eventLabel and matching timeouts alongside the existing background command registration.
Lifecycle hook dispatch and timing tests
cmuxTests/CLIGenericHookPersistenceTests.swift
Tests validate background dispatch with nohup sh -c for lifecycle hooks (while feed hooks remain foreground), and verify lifecycle hook commands return before detached handlers finish by running against a fake cmux that sleeps before writing a marker file. New waitForFile helper polls for file existence until timeout.
Execution model documentation
docs/agent-hooks.md
Clarified that lifecycle hooks are fire-and-forget background processes that stage payloads and immediately return {}, while Feed bridge hooks run foreground with longer timeout for approval/denial responses.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • manaflow-ai/cmux#4225: Overlaps in CLI/CMUXCLI+AgentHookDefinitions.swift refactoring of agentHookShellCommand and legacy hook parsing logic.
  • manaflow-ai/cmux#4409: Modifies the same hook-command routing in hookCommandString and agentHookShellCommand helpers for lifecycle-hook execution via pinned-dispatch markers.

Poem

🐰 Background hooks now fire and forget,
No blocking waits to cause regret,
With nohup workers dancing free,
Payloads staged so swift and spry—
Tests ensure the fast goodbye! ✨


Important

Pre-merge checks failed

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

❌ Failed checks (4 errors, 1 warning)

Check name Status Explanation Resolution
Cmux No Hacky Sleeps ❌ Error Production shell code uses sleep 1 polling (kill -0 in loop) for process lifecycle synchronization instead of event-driven approach, violating runtime-no-hacky-sleeps rule. Replace sleep-based polling loop with wait builtin or SIGCHLD trap to properly handle process termination.
Cmux Full Internationalization ❌ Error New localization key cli.hooks.kiro.postInstallNote has only 3 locales (en, ja, ko) while catalog supports 13+ including bs, da, de, es, fr, it, pl, ru, zh-Hans, zh-Hant. Add missing locale translations for cli.hooks.kiro.postInstallNote in Resources/Localizable.xcstrings to match all existing locales in the catalog.
Cmux Architecture Rethink ❌ Error PR introduces sleep-based wait/kill loop in production workerScript to manage background child process lifecycle—a timing repair pattern papering over need for async dispatch. Replace sleep-based lifecycle management with deterministic completion signaling (socket, named-pipe, or task group), or document why sleep-managed background dispatch is architecturally unavoidable.
Cmux Source Artifacts ❌ Error 86+ artifact files added from .claude/, .vercel/, .next/, .swiftpm/, artifacts/, .agents/, .greptile/, .cursor/, .coderabbit.yaml—all prohibited per source-control-artifacts.md. Remove all files from .claude/, .vercel/, .next/, .swiftpm/, artifacts/, .agents/, .greptile/, .cursor/, .coderabbit.yaml and add to .gitignore.
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (16 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: running agent lifecycle hooks in the background instead of synchronously.
Description check ✅ Passed The description covers the summary section with what changed and why, includes detailed verification steps with manual testing results, but lacks a demo video and incomplete checklist items.
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 CLI code adds pure value types and static methods without MainActor issues; no problematic protocol conformances or shared mutable types; no actor isolation debt worsened.
Cmux Swift Blocking Runtime ✅ Passed No production Swift blocking primitives introduced. Shell script sleep 1 is shell-level, not Swift. Test-only usleep(50_000) in waitForFile() helper is permitted per guidelines.
Cmux Expensive Synchronous Load ✅ Passed PR adds no expensive synchronous loaders. Changes only generate shell command strings for hook setup, not on main-actor or interactive UI paths.
Cmux Cache Substitution Correctness ✅ Passed codexLegacyHookTrustHashes is computed fresh, not cached. It adds trust for both new and legacy hook formats for backward compatibility without replacing authoritative file reads.
Cmux Algorithmic Complexity ✅ Passed Production code iterates over fixed-size agent definitions (16 agents, max 8 events) with O(1) operations; test code uses bounded polling for file synchronization.
Cmux Swift Concurrency ✅ Passed PR introduces shell command builders and tests, not Swift concurrency patterns. No DispatchQueue, Task, or Combine anti-patterns; test-only usleep is allowed per rules.
Cmux Swift @Concurrent ✅ Passed New functions are synchronous pure-string manipulations with no async, @concurrent, or isolation attributes. No concurrency rule violations detected.
Cmux Swift File And Package Boundaries ✅ Passed AgentHookDefinitions (728 lines, +27) has focused hook-command-generation responsibility; cmux.swift adds only 5 lines to 32k file (below 250 threshold); test file allowed by exception.
Cmux Swift Logging ✅ Passed All logging in the PR complies with swift-logging.md: print() is CLI output or debug function (allowed), no NSLog/debugPrint/dump in app code.
Cmux User-Facing Error Privacy ✅ Passed Production code returns only '{}' with no exposed vendor names, credentials, env vars, or internal details. Tests and docs are allowed categories.
Cmux Swiftui State Layout ✅ Passed PR contains no SwiftUI code. All changes are CLI tool implementation (hook command generation, shell scripting, tests, docs). SwiftUI state layout rules don't apply.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR adds background agent hook execution via shell commands and tests; no NSWindow, NSPanel, NSWindowController, or SwiftUI Window/WindowGroup code present. Check not applicable.
✨ 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-background-agent-hooks

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.

@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 252da29. Configure here.

) -> String {
let foregroundCommand = agentHookShellCommand(command, for: def)
let workerScript = """
payload="${CMUX_HOOK_PAYLOAD:-}"; command="${CMUX_HOOK_COMMAND:-}"; cleanup() { [ -n "$payload" ] && rm -f "$payload"; }; trap cleanup EXIT INT TERM; [ -n "$payload" ] && [ -n "$command" ] || exit 0; ( eval "$command" ) < "$payload" & child=$!; remaining=30; while [ "$remaining" -gt 0 ]; do kill -0 "$child" 2>/dev/null || break; sleep 1; remaining=$((remaining - 1)); done; if kill -0 "$child" 2>/dev/null; then kill "$child" 2>/dev/null || true; fi; wait "$child" 2>/dev/null || 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.

Background worker kills slow hooks

Medium Severity

The detached nohup worker waits at most 30 seconds, then sends kill to the child running the real cmux hooks handler. Lifecycle work that legitimately exceeds 30 seconds (slow app IPC, restore, or naming) can be terminated mid-flight while the agent already received {}, so session state, Feed telemetry, and restore records may never update.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 252da29. Configure here.

@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: 252da29162

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

} else {
prerequisite = "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"
}
return ": \(backgroundHookMarker); \(prerequisite) cmux_hook_payload=\"$(mktemp \"${TMPDIR:-/tmp}/cmux-\(logSlug).json.XXXXXX\")\" || { echo '{}'; exit 0; }; cat > \"$cmux_hook_payload\" || true; cmux_hook_log_dir=\"${TMPDIR:-/tmp}/cmux-agent-hooks\"; mkdir -p \"$cmux_hook_log_dir\" 2>/dev/null || true; CMUX_HOOK_PAYLOAD=\"$cmux_hook_payload\" CMUX_HOOK_COMMAND=\(shellSingleQuote(foregroundCommand)) nohup sh -c \(shellSingleQuote(workerScript)) >> \"$cmux_hook_log_dir/\(logSlug).log\" 2>&1 & 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 PID-based routing for pinned hooks

When this wrapper is used for Grok or Antigravity, the foreground handler no longer runs as a child of the agent hook process: the original hook shell exits after nohup ... &, so the worker is reparented before it invokes cmux hooks .... Those pinned integrations are explicitly handled in runGenericAgentHook as agents that may strip CMUX_* and therefore rely on inferredAgentPID()/process binding to find the target surface; after detaching, that inference walks through the worker shell to launchd instead of the agent, so new session-start/prompt/stop hooks can resolve no workspace and silently no-op. Keep pinned/PID-routed hooks foreground or pass an explicit workspace/surface/PID into the background worker.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR converts generic agent lifecycle hooks (session-start, prompt-submit, stop, etc.) from synchronous foreground calls to fire-and-forget background dispatchers, while keeping Feed bridge hooks foreground so approval decisions and exit-code propagation still work. It also retains the old synchronous command string as a trust-hash entry so Codex installations upgraded with the new CLI continue to recognize previously approved hook commands.

  • CMUXCLI+AgentHookDefinitions.swift: Renames the old entry point to legacySynchronousHookCommandString, adds backgroundAgentHookShellCommand which writes stdin to a temp file, launches the real hook handler under nohup sh -c, and immediately returns {}. A 30-second worker-side timeout kills the child if it runs long.
  • cmux.swift: Adds a second insertHashes call for the legacy synchronous command so three hash variants (new background, old synchronous, legacy codex-hook) are all trusted during rollout.
  • CLIGenericHookPersistenceTests.swift: Two new tests — one asserting structural properties of the generated command strings and one end-to-end integration test that exercises the actual detached-dispatch timing against a fake slow handler.

Confidence Score: 3/5

Safe to merge after addressing the sleep-polling loop in the worker script; the backward-compat hash strategy and feed-foreground split are sound.

The background dispatch logic is correct and well-tested, but the worker script implements its 30-second child timeout via a while/sleep 1 counter loop — a wall-clock polling primitive the repo's no-hacky-sleeps rule explicitly prohibits in production shell code. On a machine where hooks fire frequently the polling overhead is real, and if timeout(1) is available the loop is unnecessary.

CLI/CMUXCLI+AgentHookDefinitions.swift — the worker script embedded in backgroundAgentHookShellCommand needs attention for the polling loop and the unbounded log file.

Important Files Changed

Filename Overview
CLI/CMUXCLI+AgentHookDefinitions.swift Adds backgroundAgentHookShellCommand and renames the old entry point to legacySynchronousHookCommandString. The new worker script detaches via nohup correctly, but uses a sleep 1 polling loop to enforce its 30-second child timeout and logs to unbounded files.
CLI/cmux.swift Inserts a second insertHashes call for legacySynchronousHookCommandString alongside the new background-dispatch hash and the existing codex-hook hash, giving Codex three accepted trust-hash variants for backward compatibility during upgrade. Small, focused change.
cmuxTests/CLIGenericHookPersistenceTests.swift Adds two new tests: one structural and one end-to-end integration test that exercises detached-dispatch timing against a fake slow handler. The waitForFile helper uses usleep polling, acceptable for test scaffolding.
docs/agent-hooks.md Adds two paragraphs explaining fire-and-forget lifecycle hooks and the rationale for keeping Feed bridge hooks foreground. Documentation-only change, accurate to the implementation.

Sequence Diagram

sequenceDiagram
    participant Agent
    participant HookShell as "Hook shell script"
    participant Worker as "nohup sh -c (worker)"
    participant cmux as "cmux hooks agent event"

    Agent->>HookShell: "invoke hook (stdin = JSON payload)"
    HookShell->>HookShell: "mktemp -> write stdin to temp file"
    HookShell->>Worker: "CMUX_HOOK_PAYLOAD=... nohup sh -c '...' &"
    HookShell-->>Agent: "echo '{}' (immediate return)"

    Worker->>Worker: "trap cleanup EXIT INT TERM"
    Worker->>cmux: "eval CMUX_HOOK_COMMAND < payload_file (background)"
    Worker->>Worker: "poll kill -0 child + sleep 1 (up to 30 s)"
    cmux-->>Worker: "exit (workspace state updated)"
    Worker->>Worker: "cleanup: rm -f payload_file"
Loading

Reviews (1): Last reviewed commit: "Run agent lifecycle hooks in background" | Re-trigger Greptile

) -> String {
let foregroundCommand = agentHookShellCommand(command, for: def)
let workerScript = """
payload="${CMUX_HOOK_PAYLOAD:-}"; command="${CMUX_HOOK_COMMAND:-}"; cleanup() { [ -n "$payload" ] && rm -f "$payload"; }; trap cleanup EXIT INT TERM; [ -n "$payload" ] && [ -n "$command" ] || exit 0; ( eval "$command" ) < "$payload" & child=$!; remaining=30; while [ "$remaining" -gt 0 ]; do kill -0 "$child" 2>/dev/null || break; sleep 1; remaining=$((remaining - 1)); done; if kill -0 "$child" 2>/dev/null; then kill "$child" 2>/dev/null || true; fi; wait "$child" 2>/dev/null || true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Sleep-polling loop used for process lifetime cap

The worker script implements its 30-second timeout by decrementing a counter inside a while/sleep 1 loop. This is a wall-clock poll on process lifecycle — the exact pattern the runtime-no-hacky-sleeps rule flags for production shell code. If timeout(1) is available in the target environment (macOS 12+ ships /usr/bin/timeout; Linux always has it), the worker body can be simplified to a direct timeout 30 sh -c '...' invocation with no polling loop needed. If portability across macOS versions is the concern, the PR description or a comment should document that constraint so future maintainers don't reach for the same pattern elsewhere.

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!

} else {
prerequisite = "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"
}
return ": \(backgroundHookMarker); \(prerequisite) cmux_hook_payload=\"$(mktemp \"${TMPDIR:-/tmp}/cmux-\(logSlug).json.XXXXXX\")\" || { echo '{}'; exit 0; }; cat > \"$cmux_hook_payload\" || true; cmux_hook_log_dir=\"${TMPDIR:-/tmp}/cmux-agent-hooks\"; mkdir -p \"$cmux_hook_log_dir\" 2>/dev/null || true; CMUX_HOOK_PAYLOAD=\"$cmux_hook_payload\" CMUX_HOOK_COMMAND=\(shellSingleQuote(foregroundCommand)) nohup sh -c \(shellSingleQuote(workerScript)) >> \"$cmux_hook_log_dir/\(logSlug).log\" 2>&1 & 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.

P2 Unbounded log file growth

Every background hook invocation appends to ${TMPDIR:-/tmp}/cmux-agent-hooks/<logSlug>.log with no rotation or size cap. On a busy machine with frequent session-start/stop or prompt-submit events, these per-slug log files can grow without bound. Even on macOS where /tmp is periodically purged by the OS, the directory under a custom TMPDIR is not. Adding a tail -c N trim or a size check before the append would bound the on-disk footprint.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@cmuxTests/CLIGenericHookPersistenceTests.swift`:
- Around line 3790-3799: waitForFile currently returns as soon as the path
exists, which can race with the writer creating an empty file; update
waitForFile to also verify the marker content is fully written before returning
by reading the file and ensuring either (a) the file contains the expected
marker string or (b) the file size is > 0 and stable across two successive polls
(e.g., read size, sleep, reread size) to confirm no in-progress writes;
reference the waitForFile function and the code that reads the marker so the
check matches the expected marker content or stability criterion.
🪄 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: 5fec55ab-69f4-433a-9d1e-5bd9ec6baee5

📥 Commits

Reviewing files that changed from the base of the PR and between 39b6a6f and 252da29.

📒 Files selected for processing (4)
  • CLI/CMUXCLI+AgentHookDefinitions.swift
  • CLI/cmux.swift
  • cmuxTests/CLIGenericHookPersistenceTests.swift
  • docs/agent-hooks.md

Comment on lines +3790 to +3799
private func waitForFile(at url: URL, timeout: TimeInterval) -> Bool {
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
if FileManager.default.fileExists(atPath: url.path) {
return true
}
usleep(50_000)
}
return FileManager.default.fileExists(atPath: url.path)
}

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

waitForFile can return before marker content is fully written.

Line 3793 checks only path existence, but the writer creates the file before finishing printf, so Line 1091 can read partial content and flake.

Suggested fix
-private func waitForFile(at url: URL, timeout: TimeInterval) -> Bool {
+private func waitForFile(at url: URL, timeout: TimeInterval, minSize: UInt64 = 1) -> Bool {
     let deadline = Date().addingTimeInterval(timeout)
     while Date() < deadline {
-        if FileManager.default.fileExists(atPath: url.path) {
+        if let attrs = try? FileManager.default.attributesOfItem(atPath: url.path),
+           let size = attrs[.size] as? NSNumber,
+           size.uint64Value >= minSize {
             return true
         }
         usleep(50_000)
     }
-    return FileManager.default.fileExists(atPath: url.path)
+    if let attrs = try? FileManager.default.attributesOfItem(atPath: url.path),
+       let size = attrs[.size] as? NSNumber {
+        return size.uint64Value >= minSize
+    }
+    return false
 }
🤖 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/CLIGenericHookPersistenceTests.swift` around lines 3790 - 3799,
waitForFile currently returns as soon as the path exists, which can race with
the writer creating an empty file; update waitForFile to also verify the marker
content is fully written before returning by reading the file and ensuring
either (a) the file contains the expected marker string or (b) the file size is
> 0 and stable across two successive polls (e.g., read size, sleep, reread size)
to confirm no in-progress writes; reference the waitForFile function and the
code that reads the marker so the check matches the expected marker content or
stability criterion.

@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

This branch was successfully deployed

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

2 participants