Skip to content

Fix Codex no-TTY hook live surface misroutes - #6975

Closed
austinywang wants to merge 25 commits into
mainfrom
issue-5676-codex-hook-no-tty-misroute-corrupts-a-live-se
Closed

austinywang wants to merge 25 commits into
mainfrom
issue-5676-codex-hook-no-tty-misroute-corrupts-a-live-se

Conversation

@austinywang

@austinywang austinywang commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #5676

Summary

  • Add behavioral mock-socket regressions for no-TTY Codex hooks inheriting another live session surface.
  • Guard generic hook target resolution from routing onto a different live owner surface when no TTY/PID binding can validate the route.
  • Track PID capture time and compare OS process start time against it so recycled PIDs are treated as stale while legacy records remain conservative.
  • Clamp oversized persisted PIDs before pid_t conversion so corrupted records fail closed instead of crashing.

Testing

  • Reproduced locally with installed cmux CLI against a private mock Unix socket.
  • xcrun swiftc -parse CLI/cmux.swift cmuxTests/CLICodexHookTimeoutRegressionTests.swift cmuxTests/CLIGenericHookPersistenceTests.swift
  • python3 scripts/swift_file_length_budget.py --budget .github/swift-file-length-budget.tsv
  • ./tests/test_ci_swift_file_length_budget.sh
  • ./scripts/lint-pbxproj-test-wiring.sh
  • git diff --check
  • Autoreview clean on commit 7e168f303376d00e05b23fd1d72791236e8cad77.

Demo Video

N/A - CLI hook routing fix with mock-socket regression coverage.

Review Trigger

Bug fix for issue #5676, preserving the required two-commit failing-test then fix structure and subsequent review/CI follow-up commits.

Checklist

  • Regression test committed before the fix.
  • Fix committed separately from the failing regression.
  • No reload.sh, reload-cloud.sh, or bare xcodebuild run per task instruction.
  • Localization audit: no user-facing strings changed; no localization files required for this PR.
  • Required GitHub checks are green; conditional required checks that do not apply are skipped by CI.

Summary by CodeRabbit

  • Bug Fixes
    • Improved session liveness checks to prevent PID-reuse from causing incorrect routing.
    • Prevented target/surface takeover when a different live session instance still owns the surface.
    • Allowed safe takeover only after the previous owner’s process is no longer live for that captured instance (stopped/recycled cases).
  • Tests
    • Added regression tests covering no-TTY Codex hook behavior for both blocked takeover and successful handoff.
    • Extended mock RPC handling to support additional methods used by the test suite.

@vercel

vercel Bot commented Jun 26, 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 Jul 5, 2026 4:33am
cmux-staging Building Building Preview, Comment Jul 5, 2026 4:33am

@coderabbitai

coderabbitai Bot commented Jun 26, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@austinywang, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 13 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 86080283-08a5-4efd-9ba5-3b033f602462

📥 Commits

Reviewing files that changed from the base of the PR and between c6a51fd and ee4e688.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (2)
  • CLI/cmux.swift
  • cmuxTests/CLICodexHookTimeoutRegressionTests.swift
📝 Walkthrough

Walkthrough

CLI/cmux.swift now records PID capture time, validates liveness by PID instance, and blocks target resolution when a different live surface owner exists. cmuxTests/CLICodexHookTimeoutRegressionTests.swift adds regression coverage for blocked routing and allowed takeover, plus mock socket responses for additional RPC methods.

Changes

PID-instance liveness and routing guard

Layer / File(s) Summary
PID capture timestamp and liveness validation
CLI/cmux.swift
Adds pidCapturedAt to session records, stores capture time alongside PID updates, replaces process-existence checks with processIsLiveForSession(pid:capturedAt:), and adds processStartTime(pid:) plus live-owner detection based on runtime status.
Routing suppression via live-owner check
CLI/cmux.swift
Adds targetBlockedByDifferentLiveOwner and targetUnlessBlockedByDifferentLiveOwner, then applies them to direct-workspace-arg, binding-based, and mapped target resolution paths.
Regression tests and mock server extension
cmuxTests/CLICodexHookTimeoutRegressionTests.swift
Adds no-TTY Codex hook regression tests for blocked routing and takeover behavior, and extends mockSocketResponse to handle debug.terminals and system.top.

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

Possibly related issues

  • #5393: Also concerns preventing incorrect surface.resume.set publication and resume-binding synthesis in hook routing paths.
  • #6711: Also concerns Codex hook/session routing and surface resume ownership behavior.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main fix: preventing no-TTY Codex hook misroutes onto a live surface.
Description check ✅ Passed The description follows the template with Summary, Testing, Demo Video, Review Trigger, and Checklist sections filled in.
Linked Issues check ✅ Passed The routing guard, live-owner checks, and PID start-time validation address the no-TTY misroute and recycled-PID requirements from #5676.
Out of Scope Changes check ✅ Passed The changes stay focused on the routing fix and regression tests, with no clear unrelated code additions.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-5676-codex-hook-no-tty-misroute-corrupts-a-live-se

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 Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes the no-TTY Codex hook surface-misroute bug (#5676) by adding pidCapturedAt timestamps to persisted session records and comparing the OS process start time against the capture timestamp to detect recycled PIDs. A new surfaceHasDifferentLiveOwner guard — snapshotting candidates under the store lock, performing sysctl liveness checks outside the lock, and re-confirming ownership under the lock before clearing stale records — blocks generic target resolution from routing a hook onto a surface that still has a live owner process.

  • ClaudeHookSessionRecord gains pidCapturedAt, and processIsLiveForSession replaces the bare processExists check, returning false when the OS-reported process start time post-dates the stored capture time (recycled PID) and clamping oversized persisted PIDs before pid_t conversion.
  • surfaceHasDifferentLiveOwner uses a snapshot-outside-lock pattern to avoid holding the store lock across sysctl syscalls, with a double-check under the lock before clearing a stale owner record.
  • Regression tests cover blocked takeover for running/needsInput/legacy/newer owner statuses and permitted takeover for idle, stale-capture, and oversized-PID owners.

Confidence Score: 5/5

Production logic is correct and safe to merge; the only gap is a duplicated test case that leaves one regression scenario unexercised.

The production surfaceHasDifferentLiveOwner guard, recycled-PID detection, and pidCapturedAt tracking are all correctly implemented. The snapshot-outside-lock pattern avoids holding the store lock across sysctl calls, the pid_t clamp prevents integer overflow on corrupted records, and legacy records without pidCapturedAt continue to be treated conservatively. The one concern is in the test file: the "newerRunning" owner case is identical to "running", leaving the pre-seeded newcomer session / incomingSurfaceId-preservation branch untested, but this does not affect shipping behavior.

The codexHookWithoutTTYDoesNotRouteOntoDifferentLiveSurfaceOwner loop in CLICodexHookTimeoutRegressionTests.swift — verify whether the "newerRunning" case should have ownerStartedAfterIncoming: true.

Important Files Changed

Filename Overview
CLI/cmux.swift Adds pidCapturedAt field to session records, upgrades processExists to processIsLiveForSession with recycled-PID detection via sysctl, and introduces surfaceHasDifferentLiveOwner with snapshot-outside-lock pattern to block misroutes onto a live owner's surface. Logic is correct; production paths are well-guarded.
cmuxTests/CLICodexHookTimeoutRegressionTests.swift Adds regression tests for no-TTY Codex hook misrouting. The "newerRunning" owner case has ownerStartedAfterIncoming: false, making it identical to the "running" case — the ownerStartedAfterIncoming: true branch (pre-seeded newcomer session + surfaceId preservation check) is never exercised.
.github/swift-file-length-budget.tsv Updates budget for CLI/cmux.swift (+188 lines) and CLICodexHookTimeoutRegressionTests.swift (739→1083); also reflects a reduction in GhosttySurfaceView.swift. All changes match the diff.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Hook as no-TTY Codex Hook
    participant Store as ClaudeHookSessionStore
    participant OS as sysctl (kernel)

    Hook->>Store: surfaceHasDifferentLiveOwner(workspace, surface, incomingSession)
    Store->>Store: withLockedState — snapshot candidates
    Store-->>Hook: candidate list (sessionId, pid, pidCapturedAt, updatedAt)

    loop for each candidate (outside lock)
        Store->>OS: kill(pid, 0) — process exists?
        OS-->>Store: 0 / EPERM / ESRCH
        alt process dead or PID recycled
            Store->>Store: withLockedState — clear runtimeStatus
        else process live and same instance
            Store->>Store: withLockedState — confirm still owns surface
            Store-->>Hook: true → block route
        end
    end
    Store-->>Hook: false → allow route

    Hook->>Hook: targetUnlessBlockedByDifferentLiveOwner returns nil
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Hook as no-TTY Codex Hook
    participant Store as ClaudeHookSessionStore
    participant OS as sysctl (kernel)

    Hook->>Store: surfaceHasDifferentLiveOwner(workspace, surface, incomingSession)
    Store->>Store: withLockedState — snapshot candidates
    Store-->>Hook: candidate list (sessionId, pid, pidCapturedAt, updatedAt)

    loop for each candidate (outside lock)
        Store->>OS: kill(pid, 0) — process exists?
        OS-->>Store: 0 / EPERM / ESRCH
        alt process dead or PID recycled
            Store->>Store: withLockedState — clear runtimeStatus
        else process live and same instance
            Store->>Store: withLockedState — confirm still owns surface
            Store-->>Hook: true → block route
        end
    end
    Store-->>Hook: false → allow route

    Hook->>Hook: targetUnlessBlockedByDifferentLiveOwner returns nil
Loading

Reviews (13): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

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

@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 `@CLI/cmux.swift`:
- Around line 1313-1327: The processIsLiveForSession(pid:capturedAt:) check
still force-casts pid to pid_t without an upper bound, which can trap for
corrupted session data. Add the same Int32.max clamp used by
processStartTime(pid:) before calling kill(pid_t(pid), 0), and return false when
pid is nil, non-positive, or exceeds the safe range so the session validation
fails closed.
🪄 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: 46db665d-48bb-43a2-a4f8-277caf5198ec

📥 Commits

Reviewing files that changed from the base of the PR and between 58830db and ad16592.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (2)
  • CLI/cmux.swift
  • cmuxTests/CLICodexHookTimeoutRegressionTests.swift

Comment thread CLI/cmux.swift
…-no-tty-misroute-corrupts-a-live-se

# Conflicts:
#	.github/swift-file-length-budget.tsv
…-no-tty-misroute-corrupts-a-live-se

# Conflicts:
#	.github/swift-file-length-budget.tsv
…-no-tty-misroute-corrupts-a-live-se

# Conflicts:
#	.github/swift-file-length-budget.tsv
…-no-tty-misroute-corrupts-a-live-se

# Conflicts:
#	.github/swift-file-length-budget.tsv
…-no-tty-misroute-corrupts-a-live-se

# Conflicts:
#	.github/swift-file-length-budget.tsv
…-no-tty-misroute-corrupts-a-live-se

# Conflicts:
#	.github/swift-file-length-budget.tsv
@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026

This branch was successfully deployed

1 active deployment
Preview – cmux — ee4e6880 Deployed Jul 5, 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.

Codex hook: no-TTY misroute corrupts a live session's whole surface (binding + agent PID + lifecycle), not just the binding — facet of #5333

3 participants