Skip to content

Fix Claude shim mutual exec loop - #7010

Merged
austinywang merged 16 commits into
mainfrom
issue-7009-claude-shim-mutual-exec-loop
Jun 29, 2026
Merged

austinywang merged 16 commits into
mainfrom
issue-7009-claude-shim-mutual-exec-loop

Conversation

@austinywang

@austinywang austinywang commented Jun 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • add a regression harness for a mutual claude shim exec loop
  • add a PID-scoped hop guard to cmux-claude-wrapper so same-process shim ping-pong fails loudly instead of spinning
  • keep legitimate child claude launches fresh by resetting inherited guard state when the wrapper is entered from a different PID

Fixes #7009

Follow-up: issue fix #2, persisting a Claude binary path in cmux.json so the setting survives app relaunch/self-updater clean environments, remains separate from this intrinsic loop breaker.

Tests

  • python3 tests/test_claude_wrapper_mutual_shim_loop.py
  • python3 tests/test_claude_wrapper_user_binary_resolution.py
  • python3 tests/test_claude_wrapper_hooks.py
  • bash -n Resources/bin/cmux-claude-wrapper

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


Summary by cubic

Prevents infinite mutual claude shim exec loops with a hop‑limited guard and stricter shim detection. Restores hook metadata and NODE_OPTIONS on shim re‑entry while allowing valid child launches, finite shim chains, and real shell launchers (e.g., @anthropic-ai/claude-code/cli.js). Fixes #7009.

  • Bug Fixes
    • Added hop‑limited, per‑target re‑exec guard with newline‑delimited target history (handles ':'), persists across script re‑entry, and resets at the first non‑shim boundary; on loop, exits 126 with hints (CMUX_CUSTOM_CLAUDE_PATH or set Claude Binary Path); guard env is not leaked to passthrough real claude.
    • On shim re‑entry, forwards existing --settings/--session-id, refreshes per‑process hook metadata (CMUX_CLAUDE_PID, CMUX_CLAUDE_HOOK_CMUX_BIN), restores NODE_OPTIONS (reinstalls cmux --require while preserving original flags), clears inherited auth env (e.g., CLAUDECODE), and keeps argv unchanged to avoid duplicate hook injection; narrowed shim detection scans shebang/first lines and common shell/Node/Python re‑exec patterns, treats Node‑shebang scripts and the @anthropic-ai/claude-code shell launcher as non‑shims, skips asdf/mise/volta, and routes execs through one launcher.

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

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Prevented infinite re-execution when the claude target appears to be part of a shim loop, failing safely with a conflict/remedy message.
    • Improved re-entry behavior and ensured interactive sessions don’t duplicate hook injection.
    • Refined how node launch options are restored/merged during wrapper operation.
  • Tests
    • Added a new subprocess regression suite covering mutual shim loops, foreign shim chains (including indirect shells), and valid child execution.
    • Added assertions for correct guard cleanup and reliable hook/PID behavior in interactive scenarios.
  • Chores
    • Updated CI to run the new regression as part of existing CLI checks.

@vercel

vercel Bot commented Jun 27, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Canceled Canceled Jun 27, 2026 5:29am
cmux-staging Building Building Preview, Comment Jun 27, 2026 5:29am

@coderabbitai

coderabbitai Bot commented Jun 27, 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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b26f086a-5824-4cd9-9465-c7786cc2177b

📥 Commits

Reviewing files that changed from the base of the PR and between 650e8f3 and 6efcb54.

📒 Files selected for processing (2)
  • Resources/bin/cmux-claude-wrapper
  • tests/test_claude_wrapper_mutual_shim_loop.py

📝 Walkthrough

Walkthrough

The wrapper now detects shim-loop re-entry, routes resolved Claude launches through a guarded exec helper, refreshes re-entry metadata and Node options, and adds regression coverage for mutual loops, finite chains, child execution, interactive hook behavior, and CI execution.

Changes

Claude wrapper re-exec guarding

Layer / File(s) Summary
Re-exec guard state
Resources/bin/cmux-claude-wrapper
Adds hop-count validation, repeated-target tracking, target classification, and exit-126 diagnostics when the guard detects a loop or limit breach.
Guarded exec and re-entry paths
Resources/bin/cmux-claude-wrapper
Routes passthrough, hooks-disabled, session-launch, and re-entry branches through the guarded exec helper, and refreshes hook metadata plus Node options during guarded re-entry.
Mutual shim loop tests
tests/test_claude_wrapper_mutual_shim_loop.py, .github/workflows/ci.yml
Adds regression coverage for mutual shim loops, child execution, finite shim chains, custom shim tracking, interactive hook behavior, and environment cleanup, and runs the new test in the CLI no-socket regression job.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 I hopped through shims with guarded cheer,
and kept the loop beasts far from here.
The Claude path twirled, then գտ found its way,
with tidy hops and tests today.


Important

Pre-merge checks failed

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

❌ Failed checks (1 error, 2 warnings)

Check name Status Explanation Resolution
Cmux User-Facing Error Privacy ❌ Error The new loop-guard stderr copy exposes CMUX_CUSTOM_CLAUDE_PATH in a user-facing error, which the rule forbids for production text. Rewrite the error to use only product-safe language and generic next steps; move env var names and internal resolution details to logs or docs.
Linked Issues check ⚠️ Warning The PR addresses the loop guard in [#7009], but it misses [#2] entirely and does not implement the persistent cmux.json setting from [#7009]. Add the Sentry build-configuration changes for [#2] and either implement or explicitly defer the persistent custom-claude-path cmux.json setting for [#7009].
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.
✅ Passed checks (22 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: fixing the Claude shim mutual exec loop.
Description check ✅ Passed The description covers summary and testing, but it omits the demo video, review trigger block, and checklist.
Out of Scope Changes check ✅ Passed The changes stay focused on the shim-loop fix and regression tests, with no clearly unrelated additions.
Cmux Swift Actor Isolation ✅ Passed PR only adds a shell wrapper regression test and CI workflow updates; no production Swift files changed, so no new actor-isolation regressions are introduced.
Cmux Swift Blocking Runtime ✅ Passed No Swift source files are changed; the PR only touches a workflow, shell wrapper, and Python tests, so the Swift blocking-runtime rule is not applicable.
Cmux Browser Automation Off-Main ✅ Passed PR only changes the Claude wrapper, regression tests, and CI; it does not modify browser socket automation routing or wait/callback code.
Cmux Expensive Synchronous Load ✅ Passed PR changes only .github/workflows, a shell wrapper, and a Python regression test; no production Swift load-path changes were added or moved.
Cmux Cache Substitution Correctness ✅ Passed PR only changes a shell wrapper, Python regression tests, and CI; no production Swift/TS/JS cache-to-cache substitution in persistence/history/snapshot paths.
Cmux No Hacky Sleeps ✅ Passed The shell wrapper adds no fixed sleeps/timers/polling; the new waits are only test subprocess timeouts, and the workflow YAML sleeps are out of scope.
Cmux Algorithmic Complexity ✅ Passed The new scans are bounded (hop limit 16, fixed env-key lists) or linear PATH/arg parsing; no nested full scans or repeated sorts over scalable user collections were introduced.
Cmux Swift Concurrency ✅ Passed Diff only changes a workflow, shell wrapper, and Python test; no Swift files or concurrency patterns were touched.
Cmux Swift @Concurrent ✅ Passed PR only changes a workflow, a shell wrapper, and a Python test; no Swift sources were modified, so the Swift @concurrent rule doesn't apply.
Cmux Swift File And Package Boundaries ✅ Passed PR only touches CI, shell wrapper, and a Python test; no production Swift files were added or enlarged, so the boundary rule doesn’t apply.
Cmux Swiftpm Lockfiles ✅ Passed All external Package.swift deps have sibling Package.resolved, the root cmux.xcodeproj lockfile exists, and no Packages/*.gitignore ignores Package.resolved.
Cmux Swift Logging ✅ Passed The PR only changes a workflow, a bash wrapper, and a Python test; no app/runtime Swift logging was added or modified.
Cmux Full Internationalization ✅ Passed PR only changes CI, a shell wrapper, and tests; no Swift string catalogs, Info.plist, or web locale files were touched.
Cmux Swiftui State Layout ✅ Passed No SwiftUI state/layout violations appeared in the PR diff: no new ObservableObject/@Published/GeometryReader/lazy-row store refs or render-time writes.
Cmux Architecture Rethink ✅ Passed No Swift sources changed; this PR only touches bash, Python tests, and CI, so the Swift-architecture rethink rule isn’t applicable.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR only changes CI YAML, a Bash wrapper, and a Python regression test; no Swift window code was added or modified, so the shortcut rule isn’t applicable.
Cmux Source Artifacts ✅ Passed All changed paths are intentional source/config/test files; no logs, temp dirs, caches, build output, or other artifacts were added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No production Swift under Sources/ was changed; the PR only touches a shell wrapper, Python regression tests, and CI workflow.
Cmux No Ambient Global State ✅ Passed Diff touches only a shell wrapper and a Python test; no Swift source or global-state API was added.
✨ 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-7009-claude-shim-mutual-exec-loop

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 27, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a mutual claude shim exec loop by introducing a hop-limited, target-history re-exec guard in cmux-claude-wrapper. The guard propagates through the shim chain via lowercase env vars (cmux_claude_wrapper_reexec_guard / cmux_claude_wrapper_reexec_targets) that are automatically cleared when a non-shim target is executed, so real Claude children start fresh.

  • Loop guard: PID-scoped lifecycle via cmux_claude_wrapper_exec_resolved_claude; tracks unique target paths in a newline-delimited history and enforces a 16-hop ceiling, failing with exit 126 and an actionable remediation message.
  • Shim detection: New cmux_claude_wrapper_target_looks_like_reexec_shim reads the shebang and first 160 lines to match shell/Node/Python re-exec patterns while exempting asdf/mise/volta, Node-shebang scripts, and the @anthropic-ai/claude-code launcher.
  • Re-entry metadata refresh: A hops > 0 block in the main path refreshes CMUX_CLAUDE_PID, CMUX_CLAUDE_HOOK_CMUX_BIN, and NODE_OPTIONS on shim re-entry and forwards the already-processed argv unchanged to avoid double hook injection; 11 new regression tests are added to CI.

Confidence Score: 5/5

Safe to merge. The guard logic is correct and the 11 regression tests validate all the key paths including mutual loops, finite chains, passthrough env cleanup, and hook deduplication.

The core guard mechanism (hop counter + newline-delimited target history, cleared on non-shim exec) is implemented correctly. Guard vars are lowercase and are explicitly unset before reaching a non-shim target, so they never leak into real Claude's environment. The hops > 0 re-entry block correctly short-circuits hook re-injection and refreshes per-process metadata.

No files require special attention. The two notes on cmux-claude-wrapper are about an off-by-one in the target-seen check (still terminates correctly) and a stale CMUX_AGENT_LAUNCH_ARGV_B64 on re-entry, neither of which affects the loop-breaking correctness.

Important Files Changed

Filename Overview
Resources/bin/cmux-claude-wrapper Core guard implementation: hop counter + newline-delimited target history propagated via lowercase env vars, cleared on non-shim exec boundary; shim detection heuristics cover the main patterns; hops > 0 re-entry block correctly skips hook re-injection.
tests/test_claude_wrapper_mutual_shim_loop.py 11 regression tests covering mutual bash shim loop, node shim loop, indirect shell chain, finite layered shims, passthrough guard-env cleanup, colon-in-path tracking, real-shell-launcher child launch, custom wrapper child launch, interactive hook dedup, and PID refresh on shim re-entry.
.github/workflows/ci.yml One-line addition: runs test_claude_wrapper_mutual_shim_loop.py in the existing CLI tests job, correctly placed between the hooks and user-binary-resolution tests.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant shell as Shell
    participant cmux as cmux-claude-wrapper
    participant guard as reexec_guard
    participant foreign as foreign-shim/claude
    participant real as real claude binary

    shell->>cmux: exec claude [args]
    cmux->>guard: "init_reexec_guard (hops=0)"
    cmux->>cmux: find_real_claude → foreign-shim
    cmux->>guard: exec_resolved_claude(foreign-shim)
    guard->>guard: target_looks_like_shim? YES
    guard->>guard: "target_seen? NO → record (hops=1)"
    guard->>foreign: exec foreign-shim

    foreign->>foreign: adjust PATH, exec claude
    foreign->>cmux: "exec cmux-claude-wrapper (hops=1)"
    cmux->>guard: "init_reexec_guard (hops=1)"

    alt "hops > 0 AND target_seen"
        cmux->>guard: exec_resolved_claude(foreign-shim again)
        guard->>guard: target_seen? YES → fail_reexec_guard
        guard-->>shell: exit 126 + actionable error
    else finite chain
        cmux->>cmux: find_real_claude → real binary
        cmux->>guard: exec_resolved_claude(real-binary)
        guard->>guard: target_looks_like_shim? NO
        guard->>guard: unset guard vars
        guard->>real: exec real binary (clean env)
        real-->>shell: claude output
    end
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 shell as Shell
    participant cmux as cmux-claude-wrapper
    participant guard as reexec_guard
    participant foreign as foreign-shim/claude
    participant real as real claude binary

    shell->>cmux: exec claude [args]
    cmux->>guard: "init_reexec_guard (hops=0)"
    cmux->>cmux: find_real_claude → foreign-shim
    cmux->>guard: exec_resolved_claude(foreign-shim)
    guard->>guard: target_looks_like_shim? YES
    guard->>guard: "target_seen? NO → record (hops=1)"
    guard->>foreign: exec foreign-shim

    foreign->>foreign: adjust PATH, exec claude
    foreign->>cmux: "exec cmux-claude-wrapper (hops=1)"
    cmux->>guard: "init_reexec_guard (hops=1)"

    alt "hops > 0 AND target_seen"
        cmux->>guard: exec_resolved_claude(foreign-shim again)
        guard->>guard: target_seen? YES → fail_reexec_guard
        guard-->>shell: exit 126 + actionable error
    else finite chain
        cmux->>cmux: find_real_claude → real binary
        cmux->>guard: exec_resolved_claude(real-binary)
        guard->>guard: target_looks_like_shim? NO
        guard->>guard: unset guard vars
        guard->>real: exec real binary (clean env)
        real-->>shell: claude output
    end
Loading

Reviews (3): Last reviewed commit: "Restore node options on claude shim reen..." | Re-trigger Greptile

@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 `@Resources/bin/cmux-claude-wrapper`:
- Around line 98-116: The loop guard is being cleared too early in
cmux_claude_wrapper_target_is_obvious_real_claude, because any target whose
first line mentions node is treated as the real Claude binary. Tighten this
check so Node-based script shims are not considered “obvious real” and keep the
hop/seen guard intact when execing them. Only clear the guard after stronger
validation that the target is the configured real binary, preserving the
mutual-exec protection in the wrapper flow.
- Around line 51-60: The guard diagnostic in cmux-claude-wrapper should be made
user-safe and localizable instead of printing hard-coded English/vendor-specific
text. Update the stderr message path in the claude shim loop guard to use
generic localized copy, and remove the raw target emission from the
repeated-target branch so internal paths, usernames, or IDs are not exposed.
Keep the loop-detection behavior intact, but ensure any remaining user-facing
output follows the localization and redaction rules.
🪄 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: 9173d029-dd71-4c9b-9326-c45ff1030f22

📥 Commits

Reviewing files that changed from the base of the PR and between fbc6a48 and 662c1a8.

📒 Files selected for processing (3)
  • .github/workflows/ci.yml
  • Resources/bin/cmux-claude-wrapper
  • tests/test_claude_wrapper_mutual_shim_loop.py

Comment thread Resources/bin/cmux-claude-wrapper Outdated
Comment thread Resources/bin/cmux-claude-wrapper Outdated
@austinywang
austinywang merged commit 19ddb2e into main Jun 29, 2026
30 checks passed

This branch was successfully deployed

1 active deployment
Preview – cmux — 6efcb54e Deployed Jun 27, 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.

Per-surface claude shim can infinite-loop with another tool's claude shim

1 participant