Skip to content

mux: plugin manager — install sidebar plugins from git repos - #7701

Merged
lawrencecchen merged 4 commits into
mainfrom
feat-mux-plugin-mgr
Jul 9, 2026
Merged

lawrencecchen merged 4 commits into
mainfrom
feat-mux-plugin-mgr

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jul 9, 2026 •

Copy link
Copy Markdown
Contributor

Closes the sidebar-plugin loop: cmux-mux plugin install https://github.com/manaflow-ai/cmux-sidebar-fzf && cmux-mux plugin use fzf, then prefix-S in any session opens the fuzzy finder.

  • plugin install <git-url> [--name] [--force]: shallow clone to ~/.local/share/cmux/mux-plugins/ (XDG-aware), manifest validation (kind=sidebar, name [a-z0-9-_]+, run command), optional build step, executable verification.
  • plugin list/use/update/remove (+ use --builtin/disable): selection writes sidebar.plugin argv+cwd into mux.json atomically preserving all other keys, with best-effort reload-config to a running server.
  • CLI-only, no protocol/server changes. E2E test installs from a local file:// git fixture (no network in CI). Docs in spec/cli.md, spec/plugins.md, docs/configuration.md.

Local compile blocked (host zig issue) — CI is the compile gate.


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


Note

Medium Risk
The CLI runs user-supplied git URLs, build commands, and plugin binaries and rewrites mux.json, but changes stay client-side with manifest validation and no server/protocol changes.

Overview
Adds CLI-only sidebar plugin management so users can install git-hosted plugins and point mux.json at them without new control-socket commands.

cmux-mux plugin subcommands (install, list, use, disable, update, remove) live in a new plugin_manager module. Install shallow-clones into XDG-aware mux-plugins/<name>, parses cmux-plugin.toml (via new toml dep), optionally runs [build], checks the resolved [run] binary is executable, and supports --name / --force. Use / disable / builtin atomically patch only sidebar.plugin in mux.json while preserving other keys, then best-effort reload-config when the session socket is up.

The CLI layer splits verbs into socket vs local handlers, registers plugin as local (positional subcommands), adds per-verb help in --help, and documents plugin usage in main usage text. Tests cover manifest validation, help output, config RMW, and an e2e file:// git install/use/list flow; spec and configuration docs describe the workflow.

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


Summary by cubic

Adds a local plugin manager to install and manage sidebar plugins from git repos. Selecting a plugin writes sidebar.plugin to mux.json atomically and best-effort reloads the running session; no socket protocol changes.

  • New Features

    • plugin verbs: install <git-url> [--name] [--force], list [--json], use <name>|--builtin, disable, update <name>, remove <name>.
    • Installs to ~/.local/share/cmux/mux-plugins/<name> (XDG-aware), validates manifest, runs optional build, and verifies executables.
    • Resolves relative run commands to absolute paths; writes sidebar.plugin { command, cwd } via atomic RMW while preserving other mux.json keys; best-effort reload-config on use/disable/removal.
    • --help now lists concise per-verb help; integration test installs from a local file:// repo and covers build + exec verification; docs updated in spec/cli.md, spec/plugins.md, and docs/configuration.md.
  • Bug Fixes

    • Fixed CLI stream mode handling and boolean flag parsing for local verbs.
    • Tests hardened: canonicalize paths on macOS, ensure build-step creates the runnable, and create the fixture source dir explicitly.

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

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added sidebar plugin management commands to the CLI: install, list (--json), use, disable, update, and remove.
    • Improved CLI help to include the new plugin verb guidance.
  • Bug Fixes
    • Plugin selection now updates mux.json while preserving unrelated existing settings.
    • Added stricter plugin manifest validation and command executability checks before activation.
  • Documentation
    • Documented the sidebar plugin install layout, validation rules, and how use --builtin/reload behavior works.
  • Tests
    • Added unit and integration tests covering help text, manifest validation, and end-to-end plugin workflows.

cmux-mux plugin install <git-url> shallow-clones into
~/.local/share/cmux/mux-plugins/<name> (XDG-aware), validates
cmux-plugin.toml (kind, sanitized name, run command), runs the optional
build step, and verifies the executable. plugin list/use/update/remove
manage selection; use writes sidebar.plugin argv+cwd into mux.json via an
atomic read-modify-write that preserves unknown keys, and best-effort
reload-configs a running server. CLI-only (no protocol changes); e2e test
installs from a local file:// git fixture. Docs in spec/cli.md,
spec/plugins.md, docs/configuration.md.
@vercel

vercel Bot commented Jul 9, 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 9, 2026 12:16pm
cmux-staging Building Building Preview, Comment Jul 9, 2026 12:16pm

@socket-security

socket-security Bot commented Jul 9, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedcargo/​toml@​0.8.2310010093100100

View full report

@coderabbitai

coderabbitai Bot commented Jul 9, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds sidebar plugin management to cmux-mux, including CLI-local plugin verbs, config writes for sidebar.plugin, session reload signaling, and updates to docs, specs, and tests.

Changes

Sidebar Plugin Manager

Layer / File(s) Summary
Workspace dependency addition
mux/Cargo.toml, mux/crates/mux-tui/Cargo.toml
Adds toml as a workspace dependency for manifest parsing.
Sidebar plugin config write helpers
mux/crates/mux-tui/src/config.rs
Adds SidebarPluginConfig, config_path, write_sidebar_plugin, and write_sidebar_plugin_at_path to atomically update sidebar.plugin JSON while preserving other keys, plus a unit test.
CLI verb dispatch refactor for local verbs
mux/crates/mux-tui/src/cli.rs
Introduces VerbKind (Socket/Local), print_help, boolean flag parsing, positional-arg handling for local verbs, and updated run_command dispatch, with unit tests.
Plugin manager core: install/manifest/validation
mux/crates/mux-tui/src/plugin_manager.rs
Adds CliOptions, run dispatcher, install_command, manifest read/parse/validate, name validation, build execution, run-command resolution, executable verification, and git wrapper.
Plugin manager list/use/disable/update/remove commands and reload
mux/crates/mux-tui/src/plugin_manager.rs
Adds list/use/disable/update/remove commands, config-reload signaling over socket, selection lookup, install scanning, and helper utilities.
CLI entrypoint wiring for plugin subcommand
mux/crates/mux-tui/src/main.rs
Declares plugin_manager, extends USAGE with plugin verbs, and switches help output to cli::print_help.
Integration tests, docs, and spec updates
mux/crates/mux-tui/tests/cli.rs, mux/docs/configuration.md, mux/spec/cli.md, mux/spec/plugins.md
Adds integration tests against a local git fixture and updates configuration docs, CLI verb table, and plugin install-layout spec.

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

Possibly related PRs


Important

Pre-merge checks failed

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

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Cmux User-Facing Error Privacy ❌ Error report_reload_config prints the server’s raw error field in user-facing recovery copy, and send_reload_config bails with that same upstream message. Replace raw upstream/server error text with a generic cmux message and keep details only in logs or internal telemetry; don’t surface the error field directly.
Docstring Coverage ⚠️ Warning Docstring coverage is 20.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (23 passed)
Check name Status Explanation
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 The only file changed in HEAD is mux/crates/mux-tui/tests/cli.rs, so this PR introduces no Swift actor-isolation changes to review.
Cmux Swift Blocking Runtime ✅ Passed PR diff changes only mux/crates/mux-tui/tests/cli.rs; no Swift files or Swift blocking primitives are introduced.
Cmux Browser Automation Off-Main ✅ Passed PR only changes mux-tui plugin/config/CLI/docs; it doesn't touch the Swift browser-automation policy files or any browser.* routing.
Cmux Expensive Synchronous Load ✅ Passed PASS: The diff only changes mux/crates/mux-tui/tests/cli.rs (Rust tests); no Swift code or synchronous agent-history loaders were added or moved onto UI/main-actor paths.
Cmux Cache Substitution Correctness ✅ Passed Diff only touches a Rust test file; no Swift/TS/JS production persistence/snapshot code changed, so the cache-substitution rule doesn't apply.
Cmux No Hacky Sleeps ✅ Passed The only diff adds fs::create_dir_all in a test fixture; no sleeps, timers, polling, or fixed waits were introduced.
Cmux Algorithmic Complexity ✅ Passed Plugin manager does single-pass scans/sorts only; no nested rescans or hot-path unbounded filtering were introduced.
Cmux Swift Concurrency ✅ Passed No Swift files are changed in this PR diff, so the Swift concurrency rule is not applicable.
Cmux Swift @Concurrent ✅ Passed No Swift files are in the diff; only Rust, lockfile, and docs changed, so the Swift concurrent-annotation rule is not applicable.
Cmux Swift File And Package Boundaries ✅ Passed The PR diff only touches Rust/docs files; no .swift files or Swift package-boundary changes are present.
Cmux Swiftpm Lockfiles ✅ Passed No SwiftPM/Xcode/.gitignore/workflow files changed; the diff is Rust Cargo and docs only, so Package.resolved policy is not triggered.
Cmux Swift Logging ✅ Passed The diff touches only Rust/Cargo/docs files; git diff ... -- '*.swift' returned 0 files, so the Swift logging rules don't apply.
Cmux Full Internationalization ✅ Passed The diff only changes Rust CLI internals and tests; no Swift string catalogs, web/i18n files, or locale message files were touched, so the rule isn’t implicated.
Cmux Swiftui State Layout ✅ Passed Diff vs origin/main touches only Rust/docs files; no .swift or SwiftUI view/state code changed, so the SwiftUI layout rule doesn’t apply.
Cmux Architecture Rethink ✅ Passed The Swift architecture rule is inapplicable here: the PR changes Rust CLI/docs files, and no Swift files are in the diff.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR only changes Rust/tests/docs; no Swift window code was added or modified, so the auxiliary-window shortcut rule is not triggered.
Cmux Source Artifacts ✅ Passed All changed paths are intentional source/docs/config/test files; no temp/cache/build/artifact paths or generated outputs were added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed PR changes only mux/crates/mux-tui/tests/cli.rs; no Swift production Sources files were modified, so the seam rule is not applicable.
Cmux No Ambient Global State ✅ Passed No Swift files or Swift ambient-state patterns appear in the diff; this Rust-only PR is out of scope for the check.
Title check ✅ Passed The title clearly summarizes the main change: adding a plugin manager for installing sidebar plugins from git repos.
Description check ✅ Passed The description covers the change and testing, but it omits template sections like Demo Video, Review Trigger, and Checklist.
✨ 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-mux-plugin-mgr

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.

Comment thread mux/crates/mux-tui/src/cli.rs Outdated
Comment thread mux/crates/mux-tui/src/plugin_manager.rs
@greptile-apps

greptile-apps Bot commented Jul 9, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds local sidebar plugin management to cmux-mux. The main changes are:

  • New plugin install/list/use/disable/update/remove CLI commands.
  • TOML manifest parsing and plugin validation.
  • Atomic mux.json updates for sidebar.plugin.
  • Local git fixture coverage for the plugin workflow.
  • Plugin docs and CLI help updates.

Confidence Score: 4/5

This is close, but the config overwrite should be fixed before merging.

  • plugin use can still destroy an existing non-object sidebar config value.
  • The rest of the reviewed plugin workflow is scoped to the new local CLI path.

mux/crates/mux-tui/src/config.rs

Important Files Changed

Filename Overview
mux/crates/mux-tui/src/config.rs Adds atomic config writing for sidebar.plugin, but still overwrites non-object sidebar values.
mux/crates/mux-tui/src/plugin_manager.rs Adds the local plugin install, selection, update, list, and removal workflows.
mux/crates/mux-tui/src/cli.rs Adds local verb dispatch for plugin commands and updates help output.
mux/crates/mux-tui/tests/cli.rs Adds plugin CLI coverage using a local git fixture.

Reviews (4): Last reviewed commit: "mux: create the fixture source dir (remo..." | Re-trigger Greptile

fs::create_dir_all(&root)?;
let temp_dir = root.join(format!(".install-{}-{}", std::process::id(), now_nanos()));
let clone_result =
run_git(["clone", "--depth", "1", positionals[1].as_str()], Some(&temp_dir), None);

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 security Plugin URL Runs Unrestricted Code

plugin install passes any supplied repository URL straight to git clone, then the same install/update flow can execute that repo's [build].command and later persist its run command into mux.json. A copied or mistyped URL can therefore run code from an unapproved source during install and make the mux server launch it after plugin use; the installer needs a compulsory trusted-source gate or explicit confirmation before clone/build/use.

current_dir: Option<&Path>,
) -> anyhow::Result<()> {
let mut command = Command::new("git");
command.args(["-c", "protocol.file.allow=always"]).args(args);

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 security File Protocol Always Enabled

run_git enables protocol.file.allow=always for every plugin git operation, including URLs passed by end users. That removes git's local-file protection for the new installer, so a local path or file:// remote can be cloned and its manifest build command run without a separate local-source opt-in.

Comment on lines +950 to +951
if !sidebar.is_object() {
*sidebar = json!({});

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 Sidebar Value Gets Replaced

When mux.json already contains a non-object sidebar value, plugin use silently replaces that value with {} before adding sidebar.plugin. The PR promises to preserve unrelated config state, but this path destroys the existing sidebar value instead of rejecting the incompatible shape or leaving it untouched.


fn installed_plugins() -> anyhow::Result<Vec<InstalledPlugin>> {
let root = install_root()?;
let selected = selected_plugin_cwd()?;

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 Invalid Config Blocks Listing

plugin list calls selected_plugin_cwd()? before reading installed plugin directories, so a malformed mux.json makes listing fail with a parse error even though installed plugins are still on disk. The TUI loader ignores invalid config and falls back to defaults, but this new local command cannot list or inspect plugins until the config is manually repaired.

if !dir.exists() {
return Err(ManagerError::Failure(anyhow::anyhow!("plugin {name:?} is not installed")));
}
let selected = selected_plugin_cwd()?.is_some_and(|cwd| same_path(&cwd, &dir));

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 Invalid Config Blocks Removal

plugin remove <name> reads sidebar.plugin.cwd with ? before deleting the plugin directory. If mux.json is malformed, removal fails before fs::remove_dir_all, so users cannot remove an installed plugin through the CLI until they manually fix the config file.

@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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
mux/crates/mux-tui/src/cli.rs (1)

504-547: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Rename the socket-mode flag before the connection binding

stream is bound twice here: the VerbKind::Socket bool is shadowed by let mut stream = transport::connect(...), so the later if stream reads the connection handle instead of the flag and this function no longer compiles. Rename the boolean (for example is_stream) and use that for the timeout/dispatch branch.

🤖 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 `@mux/crates/mux-tui/src/cli.rs` around lines 504 - 547, The socket-mode
boolean from VerbKind::Socket is being shadowed by the transport::connect result
in cli.rs, so the later if stream branch is using the connection handle instead
of the mode flag and breaks compilation. Rename the initial flag binding in the
match (for example to is_stream) and update the timeout and final dispatch logic
in this function to use that renamed boolean while keeping the connected socket
variable as the stream handle.
🤖 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 `@mux/crates/mux-tui/src/plugin_manager.rs`:
- Around line 199-217: The update_command flow re-resolves the plugin after a
pull but never refreshes the persisted selected-plugin config, so mux.json can
keep stale sidebar.plugin data. Add the same selected-plugin check used in
remove_command: after read_manifest/run_build_if_needed/resolved_run_command, if
the updated plugin is currently selected, rewrite the stored run command and cwd
in mux.json to match the newly resolved command from update_command. Use the
existing use_command/remove_command logic as the reference for locating the
selection and config update path.
- Around line 384-402: The run_git helper is forcing git to allow file://
protocols too broadly by using protocol.file.allow=always. Update the Command
setup in run_git to use the user policy instead, since this helper is only used
for user-triggered git clone/pull flows. Keep the change localized to run_git
and only consider broader allow=always handling in any separate fixture-specific
path if needed.

In `@mux/crates/mux-tui/tests/cli.rs`:
- Around line 374-378: The CLI test is comparing non-canonical expected paths
against values produced by plugin_manager::use_command via
canonical_path/fs::canonicalize, which will differ on macOS. Update the
assertions in the cli.rs test to canonicalize the expected installed_dir and
derived bin/sidebar command path before comparing, so the
written["sidebar"]["plugin"]["cwd"] and command[0] checks match the
canonicalized paths used by the implementation.

---

Outside diff comments:
In `@mux/crates/mux-tui/src/cli.rs`:
- Around line 504-547: The socket-mode boolean from VerbKind::Socket is being
shadowed by the transport::connect result in cli.rs, so the later if stream
branch is using the connection handle instead of the mode flag and breaks
compilation. Rename the initial flag binding in the match (for example to
is_stream) and update the timeout and final dispatch logic in this function to
use that renamed boolean while keeping the connected socket variable as the
stream handle.
🪄 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: 57222e27-3826-46d0-8ab6-2dc0285b999b

📥 Commits

Reviewing files that changed from the base of the PR and between a192230 and b8e427b.

⛔ Files ignored due to path filters (1)
  • mux/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • mux/Cargo.toml
  • mux/crates/mux-tui/Cargo.toml
  • mux/crates/mux-tui/src/cli.rs
  • mux/crates/mux-tui/src/config.rs
  • mux/crates/mux-tui/src/main.rs
  • mux/crates/mux-tui/src/plugin_manager.rs
  • mux/crates/mux-tui/tests/cli.rs
  • mux/docs/configuration.md
  • mux/spec/cli.md
  • mux/spec/plugins.md

Comment on lines +199 to +217
fn update_command(positionals: &[String], options: &CliOptions) -> Result<(), ManagerError> {
reject_plugin_flags(options, false, false, false)?;
if positionals.len() != 2 {
return Err(ManagerError::Usage("usage: cmux-mux plugin update <name>".to_string()));
}
let name = &positionals[1];
validate_plugin_name(name)?;
let dir = install_root()?.join(name);
if !dir.is_dir() {
return Err(ManagerError::Failure(anyhow::anyhow!("plugin {name:?} is not installed")));
}
run_git(["pull", "--ff-only"], None, Some(&dir))?;
let manifest = read_manifest(&dir)?;
run_build_if_needed(&manifest, &dir)?;
let command = resolved_run_command(&manifest, &dir)?;
verify_executable(&command[0])?;
println!("updated {name}{}", version_suffix(&manifest));
Ok(())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

update leaves a stale sidebar.plugin for the currently-selected plugin.

use_command persists the resolved run command + cwd into mux.json, and remove_command clears it when the removed plugin is selected. But update re-resolves and verifies the command without rewriting the config. If the pulled version changes run.command (e.g. new args or binary path), the selected plugin in mux.json keeps the old argv until the user manually re-runs plugin use, so a reload picks up stale config. Mirror the selection check already used in remove_command.

🔧 Refresh config when the updated plugin is selected
     let command = resolved_run_command(&manifest, &dir)?;
     verify_executable(&command[0])?;
+    if selected_plugin_cwd()?.is_some_and(|cwd| same_path(&cwd, &dir)) {
+        let cwd = canonical_path(&dir)?;
+        let path = config::write_sidebar_plugin(Some(&SidebarPluginConfig {
+            command,
+            cwd: Some(cwd.display().to_string()),
+        }))?;
+        println!("refreshed selected sidebar.plugin in {}", path.display());
+        report_reload_config(options);
+    }
     println!("updated {name}{}", version_suffix(&manifest));
     Ok(())
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn update_command(positionals: &[String], options: &CliOptions) -> Result<(), ManagerError> {
reject_plugin_flags(options, false, false, false)?;
if positionals.len() != 2 {
return Err(ManagerError::Usage("usage: cmux-mux plugin update <name>".to_string()));
}
let name = &positionals[1];
validate_plugin_name(name)?;
let dir = install_root()?.join(name);
if !dir.is_dir() {
return Err(ManagerError::Failure(anyhow::anyhow!("plugin {name:?} is not installed")));
}
run_git(["pull", "--ff-only"], None, Some(&dir))?;
let manifest = read_manifest(&dir)?;
run_build_if_needed(&manifest, &dir)?;
let command = resolved_run_command(&manifest, &dir)?;
verify_executable(&command[0])?;
println!("updated {name}{}", version_suffix(&manifest));
Ok(())
}
fn update_command(positionals: &[String], options: &CliOptions) -> Result<(), ManagerError> {
reject_plugin_flags(options, false, false, false)?;
if positionals.len() != 2 {
return Err(ManagerError::Usage("usage: cmux-mux plugin update <name>".to_string()));
}
let name = &positionals[1];
validate_plugin_name(name)?;
let dir = install_root()?.join(name);
if !dir.is_dir() {
return Err(ManagerError::Failure(anyhow::anyhow!("plugin {name:?} is not installed")));
}
run_git(["pull", "--ff-only"], None, Some(&dir))?;
let manifest = read_manifest(&dir)?;
run_build_if_needed(&manifest, &dir)?;
let command = resolved_run_command(&manifest, &dir)?;
verify_executable(&command[0])?;
if selected_plugin_cwd()?.is_some_and(|cwd| same_path(&cwd, &dir)) {
let cwd = canonical_path(&dir)?;
let path = config::write_sidebar_plugin(Some(&SidebarPluginConfig {
command,
cwd: Some(cwd.display().to_string()),
}))?;
println!("refreshed selected sidebar.plugin in {}", path.display());
report_reload_config(options);
}
println!("updated {name}{}", version_suffix(&manifest));
Ok(())
}
🤖 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 `@mux/crates/mux-tui/src/plugin_manager.rs` around lines 199 - 217, The
update_command flow re-resolves the plugin after a pull but never refreshes the
persisted selected-plugin config, so mux.json can keep stale sidebar.plugin
data. Add the same selected-plugin check used in remove_command: after
read_manifest/run_build_if_needed/resolved_run_command, if the updated plugin is
currently selected, rewrite the stored run command and cwd in mux.json to match
the newly resolved command from update_command. Use the existing
use_command/remove_command logic as the reference for locating the selection and
config update path.

Comment on lines +384 to +402
fn run_git<const N: usize>(
args: [&str; N],
final_arg_path: Option<&Path>,
current_dir: Option<&Path>,
) -> anyhow::Result<()> {
let mut command = Command::new("git");
command.args(["-c", "protocol.file.allow=always"]).args(args);
if let Some(path) = final_arg_path {
command.arg(path);
}
if let Some(dir) = current_dir {
command.current_dir(dir);
}
let status = command.status()?;
if !status.success() {
anyhow::bail!("git failed with status {status}");
}
Ok(())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

git protocol.file.allow user vs always default top-level clone file protocol

💡 Result:

In Git, the protocol.file.allow configuration determines how the file protocol is handled during operations like cloning, fetching, or pushing [1][2]. The default policy for this protocol was changed to user in 2022 to improve security [3][4]. Policy Definitions: always: The protocol is always enabled [1]. never: The protocol is never enabled [1]. user: The protocol is enabled only when the command is initiated directly by the user (or when the environment variable GIT_PROTOCOL_FROM_USER is unset or set to 1) [1]. This setting blocks the use of the file protocol in automated, non-interactive processes, such as recursive submodule initialization, to prevent potential security vulnerabilities where a malicious repository might trick a user into executing arbitrary commands via a local file path [3][5]. Security Context: Prior to this change, the file protocol was classified as known-safe and defaulted to always [3][5]. By changing the default to user, Git prevents commands that execute without direct user input from accessing local file paths by default, mitigating risks associated with cloning from untrusted sources that might contain malicious submodules [3][4]. If you encounter errors when performing operations like submodule updates with local file paths, you can override this behavior by explicitly setting the configuration for your command: git -c protocol.file.allow=always clone --recurse-submodules [6]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="mux/crates/mux-tui/src/plugin_manager.rs"

echo "== outline =="
ast-grep outline "$file" --view expanded || true

echo
echo "== relevant lines around run_git =="
sed -n '320,470p' "$file"

echo
echo "== run_git call sites =="
rg -n "run_git\(" "$file"

Repository: manaflow-ai/cmux

Length of output: 8486


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="mux/crates/mux-tui/src/plugin_manager.rs"

echo "== install_command =="
sed -n '91,122p' "$file"

echo
echo "== update_command =="
sed -n '199,216p' "$file"

echo
echo "== git clone / pull context =="
rg -n --context 2 'clone|pull|protocol.file.allow' "$file"

Repository: manaflow-ai/cmux

Length of output: 3938


🌐 Web query:

git protocol.file.allow=user top-level git pull file:// remote allowed GIT_PROTOCOL_FROM_USER

💡 Result:

In Git, the configuration protocol.file.allow determines whether the file:// transport protocol is permitted for operations like clone, fetch, and push [1][2]. The default policy for the file protocol is set to user [3][4]. When a protocol is set to user policy: * It is allowed when the command is triggered directly by the user [1][2]. * It is disallowed for commands that execute without direct user intervention (e.g., recursive submodule initialization), unless specifically permitted [1][2]. The GIT_PROTOCOL_FROM_USER environment variable is the mechanism Git uses to distinguish between these cases [2][5]. * When GIT_PROTOCOL_FROM_USER is unset or set to 1, Git treats the operation as being initiated by the user, allowing user-policy protocols [1][6]. * When GIT_PROTOCOL_FROM_USER is set to 0, Git treats the operation as automated (or potentially untrusted), blocking user-policy protocols [2][7]. This default was changed to user to mitigate security risks, such as preventing malicious repositories from using recursive submodule cloning to exfiltrate sensitive local data [3][8]. If you encounter errors (such as during submodule operations) due to this policy, you can override it by: * Setting the configuration globally: git config --global protocol.file.allow always [9][10]. * Overriding the configuration for a specific command using the -c flag: git -c protocol.file.allow=always ... [11][10]. Note that setting protocol.file.allow in a local repository configuration (--local) may not be effective for submodules, as the submodule's own Git process does not inherit the superproject's local configuration [11][10].

Citations:


Use protocol.file.allow=user here
clone and pull are both user-triggered paths, so the default user policy already allows file:// remotes. always only widens access to non-user contexts like recursive submodule fetches, which this helper doesn’t need. If a specific local fixture requires it, scope always to that path only.

🤖 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 `@mux/crates/mux-tui/src/plugin_manager.rs` around lines 384 - 402, The run_git
helper is forcing git to allow file:// protocols too broadly by using
protocol.file.allow=always. Update the Command setup in run_git to use the user
policy instead, since this helper is only used for user-triggered git clone/pull
flows. Keep the change localized to run_git and only consider broader
allow=always handling in any separate fixture-specific path if needed.

Comment thread mux/crates/mux-tui/tests/cli.rs Outdated
Comment thread mux/crates/mux-tui/src/plugin_manager.rs
…d-step fixture

Rename the VerbKind stream flag to stream_mode (it was shadowed by the
connection binding and tested as a bool). Test compares config paths against
the canonicalized install dir (/tmp is a /private/tmp symlink on macOS).
The install fixture no longer commits the runnable: build.sh (heredoc,
shell-verified) creates it, so the build step and executable verification
are on the tested path.
Comment thread mux/crates/mux-tui/tests/cli.rs

@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 `@mux/crates/mux-tui/tests/cli.rs`:
- Around line 288-294: The test fixture setup in
plugin_install_use_and_list_work_against_local_git_repo writes cmux-plugin.toml
into source before the directory exists, which will panic. Create the source
directory first in the test setup before calling fs::write, using the existing
dir/source variables in plugin_install_use_and_list_work_against_local_git_repo
so the local git repo fixture is initialized properly.
🪄 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: 9f85740b-3eca-43f7-8b00-2f9358415e20

📥 Commits

Reviewing files that changed from the base of the PR and between caf442b and 14800b9.

📒 Files selected for processing (2)
  • mux/crates/mux-tui/src/cli.rs
  • mux/crates/mux-tui/tests/cli.rs

Comment thread mux/crates/mux-tui/tests/cli.rs

@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 using default effort 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 fd4ae0f. Configure here.

if target.exists() {
fs::remove_dir_all(&target)?;
}
fs::rename(&temp_dir, &target)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Force install deletes active plugin

High Severity

plugin install --force removes the existing install directory with remove_dir_all before moving the new clone into place, without checking whether that plugin is currently selected or stopping/reloading the sidebar. A running session can keep executing from paths under a tree that was just deleted, while mux.json still points at the old layout.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit fd4ae0f. Configure here.

Comment on lines +950 to +952
if !sidebar.is_object() {
*sidebar = json!({});
}

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 Preserve sidebar value

When mux.json already has a non-object sidebar value, plugin use reaches this branch and replaces that value with an empty object before adding sidebar.plugin. For example, a user config with "sidebar": "left" is silently rewritten and the original setting is lost. The write path should reject that incompatible shape or leave it unchanged instead of overwriting it.

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
mux/crates/mux-tui/tests/cli.rs (1)

288-407: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add test coverage for plugin update and plugin remove.

The test thoroughly covers install, list, use, and use --builtin, but plugin update <name> and plugin remove <name> are untested. Both have real logic — update does a git pull and re-runs build/verification; remove deletes the plugin directory and config entry. Regressions in these paths would go undetected.

The test infrastructure (fixture setup, plugin_cli helper, config assertions) is already in place, so adding these cases is straightforward.

♻️ Suggested additions
// After the --builtin test (line 404), before cleanup:

// --- plugin update ---
// Modify the fixture source, commit, then update.
fs::write(source.join("cmux-plugin.toml"), r#"
    [plugin]
    name = "fixture"
    kind = "sidebar"
    version = "0.2.0"
    description = "Updated fixture sidebar"

    [run]
    command = ["bin/sidebar"]

    [build]
    command = ["/bin/sh", "build.sh"]
"#).unwrap();
git(&source, &["add", "."]);
git(&source, &["-c", "user.name=cmux", "-c", "user.email=cmux@example.invalid", "commit", "-m", "v0.2.0"]);

let update = plugin_cli(&data_home, &config_path, &["plugin", "update", "fixture"]);
assert_success(&update);

// --- plugin remove ---
let remove = plugin_cli(&data_home, &config_path, &["plugin", "remove", "fixture"]);
assert_success(&remove);
assert!(!installed_dir.exists());

let list = plugin_cli(&data_home, &config_path, &["--json", "plugin", "list"]);
assert_success(&list);
let listed: serde_json::Value = serde_json::from_slice(&list.stdout).unwrap();
assert!(listed["plugins"].as_array().unwrap().is_empty());
🤖 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 `@mux/crates/mux-tui/tests/cli.rs` around lines 288 - 407, The
`plugin_install_use_and_list_work_against_local_git_repo` test is missing
coverage for `plugin update` and `plugin remove`, so extend this fixture-based
flow to exercise both commands. After the existing `plugin use --builtin`
assertions, modify and recommit the local git fixture, then invoke `plugin_cli`
with `plugin update fixture` to verify the update path succeeds and
rebuild/verification still works. Next call `plugin_cli` with `plugin remove
fixture` and assert the installed plugin directory is deleted and the config
entry is removed by checking the same `installed_dir`, `config_path`, and
`plugin list` JSON state used elsewhere in the test.
🤖 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.

Outside diff comments:
In `@mux/crates/mux-tui/tests/cli.rs`:
- Around line 288-407: The
`plugin_install_use_and_list_work_against_local_git_repo` test is missing
coverage for `plugin update` and `plugin remove`, so extend this fixture-based
flow to exercise both commands. After the existing `plugin use --builtin`
assertions, modify and recommit the local git fixture, then invoke `plugin_cli`
with `plugin update fixture` to verify the update path succeeds and
rebuild/verification still works. Next call `plugin_cli` with `plugin remove
fixture` and assert the installed plugin directory is deleted and the config
entry is removed by checking the same `installed_dir`, `config_path`, and
`plugin list` JSON state used elsewhere in the test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2fd9d872-ac9b-4f87-8e68-42733ae4ed8d

📥 Commits

Reviewing files that changed from the base of the PR and between 14800b9 and fd4ae0f.

📒 Files selected for processing (1)
  • mux/crates/mux-tui/tests/cli.rs

@lawrencecchen
lawrencecchen merged commit a1c5295 into main Jul 9, 2026
34 of 37 checks passed

This branch was successfully deployed

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

1 participant