Skip to content

Forward legacy cmux <agent>-hook commands so stale Cursor hooks return valid JSON - #4158

Closed
lawrencecchen wants to merge 2 commits into
mainfrom
feat-legacy-agent-hook-aliases
Closed

lawrencecchen wants to merge 2 commits into
mainfrom
feat-legacy-agent-hook-aliases

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented May 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Cursor agent (and any other agent with stale ~/.<agent>/hooks.json entries) was blocking every shell command with Hook "[ ... ] && cmux cursor-hook shell-exec || echo '{}'" returned invalid JSON. The command was blocked for safety.
  • Commit 6beb3dbe1 ("Namespace agent hook commands") renamed cmux <agent>-hook <sub> → cmux hooks <agent> <sub> and kept compat shims only for codex-hook and feed-hook. cursor, gemini, opencode, copilot, codebuddy, factory, qoder all lost theirs.
  • When a stale entry invokes cmux cursor-hook shell-exec, the unknown-command path prints the full CLI usage to stdout and exits non-zero. Combined with the installed || echo '{}' fallback, that produces "usage text + {}", which Cursor rejects as invalid JSON.

Fix: detect any <agent>-hook legacy command for a registered agent and forward it to runGenericAgentHook, mirroring the existing codex-hook shim across all agents. Generalize isLegacyCmuxOwnedHookCommand so reinstall purges stale legacy entries instead of layering new ones beside them.

Verification

Before/after via the exact shell wrapper Cursor invokes:

=== BEFORE (prod cmux) ===
Output bytes: 10543
Output: Error: Unknown command: cursor-hook
        cmux - control cmux via Unix socket
        Usage: ...
Parses as JSON? json.decoder.JSONDecodeError: Expecting value

=== AFTER (dev cmux) ===
Output bytes: 3
Output: '{}'
Parses as JSON? YES, valid JSON: {}

Reproduced via cursor-agent --print on a minimal hooks.json containing only the legacy entry: before fix → Hook "...cursor-hook shell-exec..." returned invalid JSON. After fix → command runs.

Test plan

  • Added testLegacyCursorHookAliasShellExecReturnsJSONWithoutHelp and testLegacyGeminiHookAliasReturnsJSONWithoutHelp in cmuxTests/CLILegacyHookAliasTests.swift, asserting exact {}\n stdout and no Usage: text. Two-commit structure: first commit adds the failing tests, second commit lands the fix.
  • Direct CLI verification across all renamed agents (cursor, gemini, opencode, copilot, codebuddy, factory, qoder, codex): each <agent>-hook session-start returns {} exit 0.
  • Unknown non-agent commands still print usage and exit 1.
  • CI E2E

🤖 Generated with Claude Code


Note

Medium Risk
Touches CLI command dispatch and hook uninstall/upgrade detection; a mistake could misroute commands or fail to purge old hook entries, but changes are limited to legacy alias handling and covered by new regression tests.

Overview
Fixes stale hook configs that still invoke legacy cmux <agent>-hook <subcommand> by detecting any registered legacy <agent>-hook command and forwarding it to runGenericAgentHook so it returns clean {} JSON instead of printing usage/unknown-command text.

Generalizes legacy hook cleanup: isLegacyCmuxOwnedHookCommand/marker lists now match old <agent>-hook and feed-hook --source <agent> entries across all agents (not just Codex) so reinstall/uninstall can purge them. Adds regression tests ensuring legacy cursor-hook and gemini-hook aliases exit 0 and emit exactly {}\n with no help/usage output.

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


Summary by cubic

Routes legacy cmux <agent>-hook commands to the namespaced dispatcher so stale hook entries return valid {} JSON instead of CLI usage text, preventing Cursor from blocking shell commands. Reinstall now detects and cleans these legacy entries across all agents.

  • Bug Fixes
    • Forward any <agent>-hook to runGenericAgentHook (cmux hooks <agent> <subcommand>).
    • Generalize legacy detection and hook markers to cover all agents, so reinstall purges stale entries.
    • Keep claude-hook/feed-hook behavior and unknown-command handling unchanged.
    • Add tests for cursor-hook shell-exec and gemini-hook session-start to assert exact {} output with no help text.

Written for commit f8a097a. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes
    • Legacy command-line aliases for agent hooks are now properly recognized and dispatched across all agent types, ensuring backward compatibility with existing automation and scripts that rely on older command formats.

Review Change Stack

lawrencecchen and others added 2 commits May 14, 2026 04:46
Stale `cmux <agent>-hook` entries in `~/.cursor/hooks.json` (and other
agent hook configs) still invoke the legacy command names removed in
commit 6beb3db. The unknown-command path prints CLI usage to stdout
and exits non-zero, which combines with `|| echo '{}'` to produce
"usage text + {}", which Cursor agent rejects as invalid JSON and uses
to block every shell execution.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Commit 6beb3db ("Namespace agent hook commands") renamed the per-agent
hook commands from `cmux <agent>-hook <subcommand>` to
`cmux hooks <agent> <subcommand>` and kept backwards-compat only for
`codex-hook` and `feed-hook`. Every other agent (cursor, gemini,
opencode, copilot, codebuddy, factory, qoder) lost its legacy alias.

Stale entries in `~/.<agent>/hooks.json` still invoke the old names.
The unknown-command path prints the full CLI usage to stdout and exits
non-zero, which combines with the `|| echo '{}'` fallback in the
installed hook command to produce "usage text + {}". Cursor agent
rejects that as invalid JSON and blocks every shell execution.

Detect any `<agent>-hook` form for a registered agent and forward it to
`runGenericAgentHook`, mirroring the existing codex-hook compat shim
across all agents. Generalize `isLegacyCmuxOwnedHookCommand` and the
hook markers to recognize the legacy form per agent so reinstall
purges stale entries instead of layering new ones beside them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented May 14, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment May 14, 2026 11:59am
cmux-staging Building Building Preview, Comment May 14, 2026 11:59am

@lawrencecchen lawrencecchen self-assigned this May 14, 2026
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented May 14, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR generalizes legacy cmux <agent>-hook command alias recognition and dispatch from Codex-only to all agents. It adds a helper to resolve agent names from legacy command names, updates hook marker generation for all agents, and modifies dispatch validation and error capture to treat these aliases as first-class, with test coverage for Cursor and Gemini aliases.

Changes

Legacy agent-hook alias generalization

Layer / File(s) Summary
Legacy hook detection and alias resolver
CLI/CMUXCLI+AgentHookDefinitions.swift
isLegacyCmuxOwnedHookCommand generalized to recognize legacy hook patterns for all agents; new legacyAgentNameFromHookCommand helper maps legacy command names (ending in -hook, excluding claude and feed) to canonical agent names via definition lookup.
Legacy hook marker generation for all agents
CLI/CMUXCLI+AgentHookDefinitions.swift
hookMarkers(for:) now unconditionally includes both current and legacy cmux <agent>-hook markers; feedHookMarkers(for:) includes both legacy feed-bridge variants for all agents instead of Codex-only conditional inclusion.
Hook command dispatch via legacy alias resolver
CLI/cmux.swift
Updated hook dispatch to treat legacy <agent>-hook aliases as first-class: expanded CMUX_SURFACE_ID/CMUX_WORKSPACE_ID validation and socket error capture to include legacy aliases, removed explicit codex-hook switch case, and added legacy shim in unknown-command path that dispatches via runGenericAgentHook when recognized.
Regression tests for legacy hook aliases
cmuxTests/CLILegacyHookAliasTests.swift
Added tests for cursor-hook shell-exec and gemini-hook session-start aliases using a shared helper that validates legacy dispatch returns {} without "Usage:" or "Unknown command" output; helper wires mock socket server and sets environment variables.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • manaflow-ai/cmux#4075: Updates legacy hook command string construction via token parsing and alias resolution in CLI/CMUXCLI+AgentHookDefinitions.swift.
  • manaflow-ai/cmux#2717: Generalizes legacy <agent>-hook command dispatch to runGenericAgentHook path with Cursor and Gemini coverage.
  • manaflow-ai/cmux#3298: Modifies hook-marker and compatibility logic to recognize and route legacy *-hook/feed-hook patterns via unified dispatcher.

Poem

🐰 Hops through the hook commands with glee,
Old cursor-hook and gemini so free,
No Codex guard to hold them back,
All agents leap upon the track,
Legacy aliases dance in the fray—
Hooray for hooks that save the day! ✨


Caution

Pre-merge checks failed

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

  • Ignore

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Cmux Architecture Rethink ❌ Error feedHookMarkers violates split ownership rule: returns agent-agnostic markers that cause unscoped cleanup, potentially removing unrelated agents' feed hooks during reinstall/uninstall cycles. Include def.name in feedHookMarkers to scope cleanup to each agent: ["cmux hooks feed --source \(def.name)", "cmux feed-hook --source \(def.name)"]
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (14 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main fix: forwarding legacy cmux <agent>-hook commands to return valid JSON instead of CLI errors.
Description check ✅ Passed The description covers the required template sections: Summary explains the problem and fix, Testing documents verification steps, and Checklist shows testing was performed locally with new tests added.
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 New static utility functions operate on value types. No shared mutable reference types, MainActor issues, or Sendable problems. Code follows Swift 6 isolation best practices.
Cmux Swift Blocking Runtime ✅ Passed No blocking primitives added. Changes add legacy hook command detection and forwarding logic only. Existing synchronization code was not modified.
Cmux No Hacky Sleeps ✅ Passed PR contains only Swift code changes. No sleep(), usleep(), or fixed-delay timing calls added in production code. Test infrastructure uses XCTest.wait(), which is approved scaffolding.
Cmux Swift Concurrency ✅ Passed No legacy async patterns introduced. New code uses synchronous functions, simple arrays, and standard error handling. No DispatchQueue, Combine, completion handlers, or fire-and-forget Tasks.
Cmux Swift @Concurrent ✅ Passed PR contains only synchronous Swift functions with no async/concurrent violations. No missing @concurrent on nonisolated async, invalid annotations, or inappropriate async work.
Cmux Swift File And Package Boundaries ✅ Passed Adds 23 lines to CLI/cmux.swift and 28 to hook definitions. Focused legacy compatibility fix well under 250-line threshold. No new domain logic or mixed responsibilities.
Cmux Swift Logging ✅ Passed All Swift code changes comply with logging rules. New files contain only legitimate CLI output and user-facing errors. No debugPrint, dump, NSLog, or ad-hoc logging violations found.
Cmux User-Facing Error Privacy ✅ Passed No violations. Legacy hooks return '{}' instead of CLI usage text. Telemetry and comments use agent names internally only. No vendor/provider names, env vars, or credentials in user output.
Cmux Swiftui State Layout ✅ Passed This PR contains no SwiftUI code. All changes are CLI Swift files and tests using Foundation, XTest, and system frameworks. The SwiftUI state layout check does not apply.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR contains only CLI command-dispatch and hook handling. No NSWindow, NSPanel, or SwiftUI Window code added/modified. Check not applicable to CLI-only changes.
✨ 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-legacy-agent-hook-aliases

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 f8a097a. Configure here.

Comment thread CLI/cmux.swift
}
if command == "setup-hooks" || command == "uninstall-hooks" { try runSetupHooks(uninstall: command == "uninstall-hooks"); return } // Backwards compatibility for old hook setup docs/scripts.
if (command == "codex-hook" || command == "feed-hook"), processEnv["CMUX_SURFACE_ID"]?.isEmpty != false, processEnv["CMUX_WORKSPACE_ID"]?.isEmpty != false,
if Self.legacyAgentNameFromHookCommand(command) != nil, processEnv["CMUX_SURFACE_ID"]?.isEmpty != false, processEnv["CMUX_WORKSPACE_ID"]?.isEmpty != false,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

feed-hook lost early-return {} outside cmux terminals

High Severity

The early-return guard that prints {} for hook commands running outside cmux terminals previously checked command == "codex-hook" || command == "feed-hook". The replacement uses Self.legacyAgentNameFromHookCommand(command), which explicitly returns nil for "feed". This means feed-hook invocations without CMUX_SURFACE_ID/CMUX_WORKSPACE_ID will no longer early-exit with {} — they'll fall through to socket connection, fail, and throw an error or block.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f8a097a. Configure here.

@greptile-apps

greptile-apps Bot commented May 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR forwards legacy cmux <agent>-hook commands (cursor, gemini, opencode, etc.) to runGenericAgentHook, fixing a regression where stale hook config entries caused Cursor and other agents to see invalid JSON because the unknown-command path printed CLI usage text to stdout.

  • legacyAgentNameFromHookCommand is added to detect any <agent>-hook argument for a registered agent and route it through the existing generic hook dispatcher; isLegacyCmuxOwnedHookCommand and hookMarkers are generalised from codex-only to all agents so reinstall correctly purges stale entries.
  • Early no-context {} exit (line 2609) now covers all legacy agent aliases via the new helper, but feed-hook is accidentally dropped from that path because the helper explicitly returns nil for \"feed\" — bare cmux feed-hook invocations without a CMUX_SURFACE_ID shell guard will now fall through to socket connection instead of returning {} gracefully.
  • Tests add cursor and gemini regression cases asserting exact {}\ stdout and zero exit code via a mock socket server.

Confidence Score: 3/5

The core alias-forwarding fix is correct and well-tested, but feed-hook's graceful no-context exit was inadvertently removed and should be restored before merging.

The agent-hook alias forwarding works correctly for all targeted agents and the new tests cover it well. However, the refactored no-context early-exit condition silently drops feed-hook coverage — users with bare cmux feed-hook entries lacking a shell-level CMUX_SURFACE_ID guard would now hit a socket connection attempt and throw instead of receiving {} immediately.

CLI/cmux.swift line 2609 — the early no-context exit condition needs feed-hook added back explicitly alongside the new generic check.

Important Files Changed

Filename Overview
CLI/cmux.swift Extends legacy alias dispatch to all registered agents via legacyAgentNameFromHookCommand; inadvertently drops feed-hook from the early no-context {} exit that was previously guarded explicitly.
CLI/CMUXCLI+AgentHookDefinitions.swift Adds legacyAgentNameFromHookCommand helper and generalises isLegacyCmuxOwnedHookCommand and hookMarkers to cover all agents, not just codex; logic appears correct.
cmuxTests/CLILegacyHookAliasTests.swift Adds integration tests for cursor-hook and gemini-hook aliases; assertions cover stdout content, exit code, and absence of usage text.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["cmux <command> ..."] --> B{legacyAgentNameFromHookCommand\nreturns non-nil?}
    B -- "Yes (e.g. cursor-hook)" --> C{CMUX_SURFACE_ID empty\nAND no --workspace/--surface?}
    C -- Yes --> D["print('{}'); return"]
    C -- No --> E[connect socket]
    B -- "No" --> F{command == 'feed-hook'?}
    F -- "Yes - no early exit" --> E
    F -- No --> G{other early exits}
    G --> E
    E --> H{switch command}
    H -- "feed-hook" --> I[runFeedHook]
    H -- "default" --> J{legacyAgentNameFromHookCommand?}
    J -- Yes --> K["runGenericAgentHook(def)"]
    J -- No --> L["print usage + throw CLIError"]
Loading

Reviews (1): Last reviewed commit: "Forward legacy `cmux <agent>-hook` comma..." | Re-trigger Greptile

Comment thread CLI/cmux.swift
Comment on lines +2609 to 2610
if Self.legacyAgentNameFromHookCommand(command) != nil, processEnv["CMUX_SURFACE_ID"]?.isEmpty != false, processEnv["CMUX_WORKSPACE_ID"]?.isEmpty != false,
!commandArgs.contains(where: { $0 == "--workspace" || $0 == "--surface" || $0.hasPrefix("--workspace=") || $0.hasPrefix("--surface=") }) { print("{}"); return } // Backwards compatibility for old installed hooks outside cmux terminals.

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 feed-hook loses its graceful no-context early exit

The original condition explicitly listed feed-hook here alongside codex-hook. The replacement uses legacyAgentNameFromHookCommand(command), but that function deliberately returns nil for "feed-hook" (candidate "feed" is excluded). So feed-hook no longer hits this path. Any legacy installation that invokes bare cmux feed-hook without a [ -n "$CMUX_SURFACE_ID" ] shell guard and runs outside a cmux terminal will now fall through to client.connect() (line 2687), which throws instead of printing {}.

Suggested change
if Self.legacyAgentNameFromHookCommand(command) != nil, processEnv["CMUX_SURFACE_ID"]?.isEmpty != false, processEnv["CMUX_WORKSPACE_ID"]?.isEmpty != false,
!commandArgs.contains(where: { $0 == "--workspace" || $0 == "--surface" || $0.hasPrefix("--workspace=") || $0.hasPrefix("--surface=") }) { print("{}"); return } // Backwards compatibility for old installed hooks outside cmux terminals.
if (Self.legacyAgentNameFromHookCommand(command) != nil || command == "feed-hook"), processEnv["CMUX_SURFACE_ID"]?.isEmpty != false, processEnv["CMUX_WORKSPACE_ID"]?.isEmpty != false,
!commandArgs.contains(where: { $0 == "--workspace" || $0 == "--surface" || $0.hasPrefix("--workspace=") || $0.hasPrefix("--surface=") }) { print("{}"); return } // Backwards compatibility for old installed hooks outside cmux terminals.

@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/CMUXCLI`+AgentHookDefinitions.swift:
- Line 333: feedHookMarkers(for:) currently generates generic markers ["cmux
hooks feed --source", "cmux feed-hook --source"] which can match and remove
hooks from other agents; update feedHookMarkers(for:) to scope markers to the
specific agent by embedding the agent identifier (use def.name) into the marker
strings so they become e.g. "cmux hooks feed --source (def.name)" / "cmux
feed-hook --source (def.name)" or otherwise include def.name in both markers,
ensuring cleanup only affects hooks belonging to that agent.
🪄 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: fbb150dc-da4a-4718-8dc4-d5066719a2ed

📥 Commits

Reviewing files that changed from the base of the PR and between 791318f and f8a097a.

📒 Files selected for processing (3)
  • CLI/CMUXCLI+AgentHookDefinitions.swift
  • CLI/cmux.swift
  • cmuxTests/CLILegacyHookAliasTests.swift

markers.append("cmux feed-hook --source")
}
return markers
["cmux hooks feed --source", "cmux feed-hook --source"]

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

Scope feed-hook removal markers to the current agent source.

feedHookMarkers(for:) is currently too broad. Matching only "cmux hooks feed --source" / "cmux feed-hook --source" can remove unrelated entries during reinstall/uninstall. Include \(def.name) in the marker so cleanup stays agent-scoped.

Suggested fix
 static func feedHookMarkers(for def: AgentHookDef) -> [String] {
-    ["cmux hooks feed --source", "cmux feed-hook --source"]
+    ["cmux hooks feed --source \(def.name)", "cmux feed-hook --source \(def.name)"]
 }
🤖 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`+AgentHookDefinitions.swift at line 333, feedHookMarkers(for:)
currently generates generic markers ["cmux hooks feed --source", "cmux feed-hook
--source"] which can match and remove hooks from other agents; update
feedHookMarkers(for:) to scope markers to the specific agent by embedding the
agent identifier (use def.name) into the marker strings so they become e.g.
"cmux hooks feed --source (def.name)" / "cmux feed-hook --source (def.name)" or
otherwise include def.name in both markers, ensuring cleanup only affects hooks
belonging to that agent.

@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 — f8a097ad Deployed May 14, 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