Repository navigation
Check the owner of the Claude shim directory in the app, workspace commands and nushell - #15185
Conversation
Move the shim directory chmod calls onto a small PrivateDirectoryCheck helper that still follows the old behavior, and add tests for a symlinked directory, a directory another user owns and a regular file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shim installer now opens the shim parent, the staging directory and the published surface directory without following a symlink, and uses each one only when it is a real directory this user owns. Anything else skips the shim instead of writing into or changing that directory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run the login-shell wrapper for a workspace's initial command in each available shell and check that CMUX_CLAUDE_WRAPPER_SHIM_ROOT lands first on PATH only when it is a real directory this user owns. Update the exact-string wrapper tests to the owner-checked form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The login-shell wrapper for a workspace's initial command now puts CMUX_CLAUDE_WRAPPER_SHIM_ROOT on PATH only when it is a directory, not a symlink, and owned by this user, in both the POSIX and fish forms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bootstrap should move only $CMUX_CLAUDE_WRAPPER_SHIM_ROOT, and only when it is a real directory this user owns. Other cmux-cli-shims entries keep their place in PATH. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The nushell bootstrap moved every PATH entry containing cmux-cli-shims to the front. It now moves only $CMUX_CLAUDE_WRAPPER_SHIM_ROOT, and only when stat reports a real directory owned by this user. Otherwise PATH keeps its order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. Warning Review limit reachedNext included review available in 4 minutes. View limit detailsLimit details: You’ve used all 10 included reviews currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Repository: manaflow-ai/cmux/.coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (11)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
|
All contributors have signed the CLA ✍️ ✅ |
The app sets CMUX_AGENT_COMMAND_SHIM_ROOT whenever any agent shim is installed, but CMUX_CLAUDE_WRAPPER_SHIM_ROOT only when the Claude shim is. With Claude integration off, the Codex, Pi, Amp and Hermes shims must still move ahead of the user's PATH prepends. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Consider CMUX_AGENT_COMMAND_SHIM_ROOT as well as CMUX_CLAUDE_WRAPPER_SHIM_ROOT, so the Codex, Pi, Amp and Hermes shims keep their place ahead of user PATH prepends when the Claude shim is off. Each root still moves only when it is a directory this user owns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI failure attributionCI passes on Written by |
|
Review: security review. Merging this, it is a clear net improvement over main, which had no owner check at all. One divergence between the three implementations is worth tightening, but it is narrower than what the PR closes and you already name it in Known limits, so I would rather have this in than hold it. The divergence, for the record The app's check is the strong one, && current.st_uid == owner
&& current.st_mode & (S_IWGRP | S_IWOTH) == 0plus it forces the directory to 0700 with The two shell-level checks stop at ownership. [ -d "$ROOT" ] && [ ! -L "$ROOT" ] && [ -O "$ROOT" ]and nushell compares So a shim root owned by the invoking user but group- or world-writable is refused by the app's own writer and trusted by both shells. That matters on a shared Mac because macOS puts every local account in Porting the bit test is small: What I checked that holds up Fails closed everywhere. Owner acceptance is correctly narrow: all three require the current euid only, and none of them accept root or any other owner. Your Symlink handling is genuinely careful, and this is the part I liked most. The app opens with Nothing legitimate breaks. The PR only touches app-generated per-surface directories under the user's private temp directory, never Homebrew's paths or root-owned ones, so Homebrew installs, admin-group setups and multi-user Macs are unaffected. Also worth noting, not a defect Trust is established once at shell start and never re-verified when The workspace-command wrapper only prepends Fixed: nothing needed to merge. Left: the group and other write-bit check in the POSIX, fish and nushell payloads, plus a test with an owned-but-group-writable directory for each, since none of the current tests cover that shape. Worth a follow-up issue so it does not get lost. |
|
Merge receipt for |
ba94a13 CI: let Iroh release gate reuse unchanged TUI artifact 71a921c fix(web): stop orphaned Cloud VM alert pages (manaflow-ai#15138) 9971c2c Keep newer iOS connections alive when a recovery is superseded (manaflow-ai#15141) c307ab0 cmux-tui: only connect to derived local sockets served by this user (manaflow-ai#15144) 1220252 codex-teams: keep the watcher's socket password out of its arguments (manaflow-ai#15140) b3a73f0 chatmux-relay: keep cmux-tui sockets and journal cursors private to this user (manaflow-ai#15156) b0d5083 ci: dispatch UI tests from a default-branch workflow; PR CI keeps no write token (manaflow-ai#15226) 1255448 test: fix three app-host tests that keep main red (manaflow-ai#15204) 0fc4975 test: pin the fixture PATH inside the zsh watcher sleep test (manaflow-ai#15237) 758aaeb fix(ios): clear read notifications on foreground return (manaflow-ai#14725) 4c15bb3 cmux-browser: stop requiring GPL for web/package.json (manaflow-ai#15231) 97fe6b4 test: keep the Cloud notification harness workspace unselected (manaflow-ai#15215) 61083e3 test: keep workspace cwd inheritance tests off the shared standard defaults (manaflow-ai#15227) eae4994 Pin password badge actions to their source runtime (manaflow-ai#14921) fd96369 Check the owner of the Claude shim directory in the app, workspace commands and nushell (manaflow-ai#15185) 0ebf8d7 Fix main-thread freeze during SSH paste detection (manaflow-ai#15113) a98c560 test: pin font magnification in the Cloud outline attention test (manaflow-ai#15213)
tests/test-execution.toml: keep this branch's shell shim test and main's workspace shim root owner test (#15185) as separate macos-shell entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
This follows #15179, which added owner checks for the Claude command shim directory (
cmux-cli-shims/<surface>) in the zsh, bash and fish integrations. It covers the three places #15179 left as follow-ups. Each of them still trusted that directory:PATH;PATH.In a shared temporary directory, another user could create
cmux-cli-shimsor a surface directory first, or put a symlink there. Now each place checks the directory first. If the check fails, it skips the shim and leavesPATHalone. Nothing moves to a new location.App shim writer.
TerminalSurface.installAgentCommandShimsIfPossiblenow checks three directories: the shim parent, the staging directory and the final surface directory. It uses a newPrivateDirectoryCheckhelper in CmuxFoundation, which:O_NOFOLLOW | O_DIRECTORY;fchmod;lstatthat the path still names that directory and that group and others can't write to it.If any directory fails, the installer returns nil and the surface starts without the shim. The old code set permissions with
FileManager.setAttributes, which follows symlinks. The helper is in CmuxFoundation so its tests can run without GhosttyKit.Workspace initial command.
WorkspaceInitialCommandLoginShellprepends$CMUX_CLAUDE_WRAPPER_SHIM_ROOTonly when it's a directory, isn't a symlink and belongs to this user. In POSIX shells the check is[ -d ] && [ ! -L ] && [ -O ]. In fish it'stest -d,not test -Landtest -O.Nushell. Nushell has no built-in owner check, so the bootstrap asks
/usr/bin/stat. Without-L, stat doesn't follow a symlink. The bootstrap used to move everyPATHentry containingcmux-cli-shimsto the front. Now it moves only the surface's shim root, and only when stat reports a directory owned byid -u. Other entries keep their order.The bootstrap looks for the shim root in two variables:
$CMUX_AGENT_COMMAND_SHIM_ROOT, which the app sets whenever any agent shim is installed;$CMUX_CLAUDE_WRAPPER_SHIM_ROOT, which the app sets only for the Claude shim.With Claude integration off, the Codex, Pi, Amp and Hermes shims keep their place ahead of the user's
PATHprepends.Known limits:
$CMUX_CLAUDE_WRAPPER_SHIM_ROOT, the same as on main.#14642 edits
TerminalSurface+AgentCommandShims.swift, its permissions tests andtests/test-execution.toml. Whichever PR lands second will need a rebase.Testing
Each fix comes after its failing test in the commit history. For each pair below, the red and green results come from the same command.
swift test --package-path Packages/macOS/CmuxFoundation --filter PrivateDirectoryCheckTestsb97e33bbf7: 5 tests, 3 failed with 6 issues (symlink, other owner, regular file)8df112be57: 5 passedpython3 tests/test_workspace_initial_command_shim_root_owner.pyd6ea06f94b: 5 tests,FAILED (failures=12); the symlink and other-owner cases failed in zsh, bash, sh, ksh, dash and fish1f92ca694f: 5 tests, OKCMUX_TEST_NU_BIN=<nu 0.113.1> python3 tests/test_nushell_shim_path_refront.py5905e00cfd: all cases passedbe36a5785b: failed;which claudefound the user's decoy, not the shimc6fd61da22: all 7 cases passedAbout the tests:
Workspace-command test. It compiles
WorkspaceInitialCommandLoginShell.swiftwith a small driver and runs the wrapped command in each available login shell. It's registered in themacos-shelllane. Its other-owner case uses/usr/share, which root owns.Nushell test. It ran with nushell 0.113.1, the checksum-verified release binary CI pins. It has four new cases:
/usr/share);cmux-cli-shimsentries staying where they are;$CMUX_AGENT_COMMAND_SHIM_ROOTnames.The other-owner case passes on main too, because the old bootstrap never moved
/usr/share.tests/test_nushell_integration_hooks.pyandtests/test_nushell_resume_command_dialect.pyalso pass with the new bootstrap.At
5905e00cfd:python3 scripts/verify-local.py --affected origin/main --swift-changed origin/mainpassed 15/15 checks.python3 scripts/ci/validate_test_execution_registry.pypassed.python3 tests/test_workspace_initial_command_shim_root_owner.pypassed.At head
c6fd61da22,python3 scripts/verify-local.py --affected origin/mainpassed 14/14 checks.Not run:
TerminalSurfaceCommandShimPermissionsTests. GhosttyKit.xcframework isn't available locally, so theTerminalSurface+AgentCommandShims.swiftchange hasn't been compiled either.WorkspaceCreateWorkingDirectoryTestswere updated but not run. Their expected strings were checked against the compiled driver's output.No user-facing strings changed.
Changelog
Checklist
🤖 Generated with Claude Code
Summary by cubic
This extends the Claude shim directory owner checks from the existing shell integrations to the three places that still trusted it: the app that writes the shims, a workspace's initial command that prepends them to
PATH, and the nushell bootstrap that refronts them. Each now uses the directory only when it is a real directory this user owns; otherwise the surface starts without the shim andPATHkeeps its order.Behavior changes
PrivateDirectoryCheckhelper that opens paths without following symlinks, sets mode 0700 viafchmod, and re-verifies ownership withlstat; it replacesFileManager.setAttributes, which followed symlinks.$CMUX_CLAUDE_WRAPPER_SHIM_ROOTonly when directory, not-symlink, and owned-by-user checks pass.$CMUX_CLAUDE_WRAPPER_SHIM_ROOTto the front ofPATH, and only when/usr/bin/stat(which doesn't follow symlinks) reports a directory owned by the user; othercmux-cli-shimsentries stay in place.Limits
CMUX_CLAUDE_WRAPPER_SHIM_ROOTisn't set, so nushell no longer moves that directory to the front ofPATH.Written for commit 5905e00. Summary will update on new commits.
Summary by CodeRabbit