Repository navigation
Add Claude Binary Path setting - #2514
Conversation
Users who install claude outside PATH (or wrap it with aliases) can now set an explicit path in Settings > Automation > Claude Code. The setting is respected by the bundled claude wrapper, claude-teams CLI, and UserDefaults fallback.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughAdds a user-configurable Claude executable path: persisted UserDefaults key and Settings UI, managed environment injection of Changes
Sequence DiagramsequenceDiagram
participant User
participant Settings as Settings UI
participant Storage as UserDefaults
participant Ghostty as GhosttyTerminalView
participant CLI as CLI/cmux.swift
participant Wrapper as Resources/bin/claude
participant Claude as Claude Binary
User->>Settings: set custom Claude path
Settings->>Storage: save claudeCodeCustomClaudePath
User->>Ghostty: launch Claude integration
Ghostty->>Storage: read customClaudePath()
alt custom path present
Ghostty->>Ghostty: set CMUX_CUSTOM_CLAUDE_PATH
end
Ghostty->>CLI: exec Claude integration (managed env)
CLI->>CLI: check CMUX_CUSTOM_CLAUDE_PATH (trim, executable, not wrapper)
alt env custom valid
CLI->>Claude: execv(custom path)
else
CLI->>Storage: read claudeCodeCustomClaudePath (trim, executable, not wrapper)
alt stored custom valid
CLI->>Claude: execv(stored custom path)
else
CLI->>Wrapper: execvp("claude")
Wrapper->>Wrapper: find_real_claude() (considers CMUX_CUSTOM_CLAUDE_PATH then PATH/bundled)
Wrapper->>Claude: exec real claude
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 44e2d6039a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if [[ -n "${CMUX_CUSTOM_CLAUDE_PATH:-}" && -x "$CMUX_CUSTOM_CLAUDE_PATH" ]]; then | ||
| printf '%s' "$CMUX_CUSTOM_CLAUDE_PATH" | ||
| return 0 |
There was a problem hiding this comment.
Prevent wrapper from exec'ing itself via custom path
In find_real_claude, the new CMUX_CUSTOM_CLAUDE_PATH branch accepts any executable path without excluding the wrapper itself. If a user sets “Claude Binary Path” to the cmux wrapper path (a realistic mistake after running which claude inside cmux), the script resolves REAL_CLAUDE to itself and repeatedly execs itself, hanging claude invocations instead of falling back to PATH. Please reject self-references (and symlinks to self) before returning the custom path.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed. Wrapper now resolves both paths and compares, skipping if custom path points to itself. Also uses -f to reject directories.
— Claude Code
Separates it from the hooks toggle so the note text clearly applies only to the integration toggle.
Greptile SummaryThis PR adds a "Claude Binary Path" setting under Settings > Automation > Claude Code Integration, allowing users to point cmux at a Key findings:
Confidence Score: 3/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant SettingsUI as Settings UI (cmuxApp.swift)
participant UserDefaults
participant TerminalSurface as GhosttyTerminalView
participant ShellWrapper as Resources/bin/claude
participant CLI as cmux claude-teams (CLI/cmux.swift)
participant RealClaude as Real claude binary
User->>SettingsUI: Enter custom claude path
SettingsUI->>UserDefaults: Store via @AppStorage(claudeCodeCustomClaudePath)
User->>TerminalSurface: Open new terminal
TerminalSurface->>UserDefaults: customClaudePath() → non-empty string?
alt Non-empty path
TerminalSurface->>TerminalSurface: setManagedEnv(CMUX_CUSTOM_CLAUDE_PATH, path)
end
User->>ShellWrapper: runs `claude` in terminal
ShellWrapper->>ShellWrapper: find_real_claude()
alt CMUX_CUSTOM_CLAUDE_PATH set & executable
ShellWrapper->>RealClaude: exec custom path
else
ShellWrapper->>ShellWrapper: scan PATH (skip self dir)
ShellWrapper->>RealClaude: exec PATH-resolved binary
end
User->>CLI: cmux claude-teams
CLI->>CLI: check CMUX_CUSTOM_CLAUDE_PATH env var + isExecutable
alt env var valid
CLI->>RealClaude: execv(custom path)
else
CLI->>UserDefaults: read claudeCodeCustomClaudePath + isExecutable
alt UserDefaults value valid
CLI->>RealClaude: execv(UserDefaults path)
else
CLI->>RealClaude: execvp("claude") via PATH / bundled binary
end
end
Reviews (1): Last reviewed commit: "Move Claude Binary Path to its own setti..." | Re-trigger Greptile |
|
|
||
| SettingsCard { | ||
| SettingsCardRow( | ||
| String(localized: "settings.automation.claudeCode.customPath", defaultValue: "Claude Binary Path"), | ||
| subtitle: String(localized: "settings.automation.claudeCode.customPath.subtitle", defaultValue: "Custom path to the claude binary. Leave empty to use PATH.") | ||
| ) { | ||
| TextField( | ||
| String(localized: "settings.automation.claudeCode.customPath.placeholder", defaultValue: "e.g. /usr/local/bin/claude"), | ||
| text: $customClaudePath | ||
| ) | ||
| .textFieldStyle(.roundedBorder) |
There was a problem hiding this comment.
Fixed-width text field may be too narrow for long paths
The TextField is constrained to 200 points, which is often insufficient for real filesystem paths (e.g., /Users/username/.volta/bin/claude or paths inside Homebrew's opt tree). Users will need to scroll within the field to verify what they typed, which makes accidental typos hard to spot.
Consider using .frame(maxWidth: .infinity) or a larger fixed width, or following the pattern used for the "Preferred Editor" field elsewhere in the view, to give users enough room to read the full path they've entered.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| let claudeExecutablePath: String? = { | ||
| // Check custom path from Settings > Automation > Claude Code | ||
| if let custom = launcherEnvironment["CMUX_CUSTOM_CLAUDE_PATH"], | ||
| !custom.isEmpty, | ||
| FileManager.default.isExecutableFile(atPath: custom) { | ||
| return custom | ||
| } | ||
| if let custom = UserDefaults.standard.string(forKey: "claudeCodeCustomClaudePath"), | ||
| !custom.isEmpty, | ||
| FileManager.default.isExecutableFile(atPath: custom) { | ||
| return custom | ||
| } |
There was a problem hiding this comment.
CMUX_CUSTOM_CLAUDE_PATH env var overrides the Settings UI silently
The two-source lookup has a subtle precedence issue: any process that injects CMUX_CUSTOM_CLAUDE_PATH into the environment before invoking cmux claude-teams will silently override whatever the user configured in Settings > Automation. This includes the GUI app itself (which sets the env var from the UserDefaults value in GhosttyTerminalView.swift), but also any external script or shell function that happens to set this variable.
In practice, for the GUI-launched path the env var and UserDefaults are always in sync (both come from the same UserDefaults key), so the env-var branch will always win for that case and the UserDefaults branch is effectively dead code in GUI-launched terminals. The UserDefaults branch is only reachable when cmux claude-teams is run directly from the shell without the app setting the env var.
This is probably intentional, but it's worth confirming that having a hidden override mechanism (env var) that takes priority over the visible Settings UI is the desired UX — particularly because there is no user-visible indication that a different path is being used when the env var is set externally.
There was a problem hiding this comment.
The env var taking precedence over UserDefaults is intentional: the app sets it per-session from the same UserDefaults value. The env var path exists so the bundled wrapper script can read the setting without accessing UserDefaults directly. In practice they always agree.
— Claude Code
| static func customClaudePath(defaults: UserDefaults = .standard) -> String? { | ||
| let value = defaults.string(forKey: customClaudePathKey) ?? "" | ||
| return value.isEmpty ? nil : value | ||
| } |
There was a problem hiding this comment.
Missing whitespace trimming before use
customClaudePath() does not trim leading/trailing whitespace from the stored value. If a user accidentally enters a path with surrounding spaces, both FileManager.default.isExecutableFile(atPath:) in the CLI and the -x check in the shell wrapper will silently fail, causing a quiet fallback to PATH resolution with no indication of why the custom path was ignored.
This is inconsistent with the existing pattern used by PreferredEditorSettings.resolvedCommand() in the same file, which applies .trimmingCharacters(in: .whitespacesAndNewlines) before the empty check. The same trimming should be applied here to stay consistent and avoid confusing silent fallbacks.
There was a problem hiding this comment.
Fixed. customClaudePath() now trims whitespace before the empty check.
— Claude Code
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@CLI/cmux.swift`:
- Around line 9691-9709: The custom-path branches for claudeExecutablePath
accept unnormalized values and don't guard against pointing at our cmux wrapper;
normalize both environment and UserDefaults values (expand ~, trim whitespace,
resolve symlinks) before using FileManager checks, and additionally call
isCmuxClaudeWrapper(on the resolved path) to reject wrapper self-references; if
the normalized path is non-empty, executable, and not isCmuxClaudeWrapper,
return it, otherwise continue to the existing
resolveClaudeExecutable/bundledClaudePath fallback.
In `@Resources/bin/claude`:
- Around line 12-14: The check for a custom Claude path currently accepts
executable directories, which lets an invalid directory bypass PATH lookup;
change the guard around CMUX_CUSTOM_CLAUDE_PATH so it validates that the path
exists as a regular file and is executable (use a file-test + executable-test)
before printing and returning, ensuring directories are rejected and the script
falls back to PATH lookup; update the conditional that references
CMUX_CUSTOM_CLAUDE_PATH in the Resources/bin/claude entry point accordingly.
In `@Sources/cmuxApp.swift`:
- Around line 3933-3936: customClaudePath(defaults:) treats a string of only
whitespace as a valid path; change it to trim whitespace/newlines (e.g., use
trimmingCharacters(in: .whitespacesAndNewlines)) on the retrieved value and
return nil when the trimmed string is empty so whitespace-only entries are
treated as unset, preserving the existing default argument and behavior
otherwise.
In `@Sources/GhosttyTerminalView.swift`:
- Around line 3759-3761: Trim the custom Claude path before exporting and avoid
setting an empty/whitespace-only value: call
ClaudeCodeIntegrationSettings.customClaudePath(), apply .trimmingCharacters(in:
.whitespacesAndNewlines) to the result, and only call
setManagedEnvironmentValue("CMUX_CUSTOM_CLAUDE_PATH", trimmedPath) if
trimmedPath is not empty; otherwise do not set the environment variable. Ensure
you reference the existing ClaudeCodeIntegrationSettings.customClaudePath()
retrieval and the setManagedEnvironmentValue call.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: f6d14e03-ea40-4c4e-ba41-f5d58d5aa5ef
📒 Files selected for processing (5)
CLI/cmux.swiftResources/Localizable.xcstringsResources/bin/claudeSources/GhosttyTerminalView.swiftSources/cmuxApp.swift
There was a problem hiding this comment.
1 issue found across 5 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="Sources/cmuxApp.swift">
<violation number="1" location="Sources/cmuxApp.swift:3934">
P2: Trim whitespace/newlines for the custom Claude path. As written, a whitespace-only value is treated as a path and still overrides PATH, so the wrapper won’t fall back to PATH even though the field appears empty.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
- Trim whitespace in customClaudePath() before returning - CLI: skip custom path if it's the cmux wrapper (isCmuxClaudeWrapper) - Wrapper: resolve real paths and skip if custom path == self - Wrapper: use -f check to reject directories
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@CLI/cmux.swift`:
- Around line 9698-9703: The loop that selects a candidate path (the for raw in
candidates block) currently only checks
FileManager.default.isExecutableFile(atPath:) and isCmuxClaudeWrapper(at:),
which allows directories to be chosen; update the selection to also verify the
candidate is a regular file (e.g., guard FileManager.default.fileExists and that
it is not a directory / is a regular file) before checking executability and
isCmuxClaudeWrapper so directory-valued custom paths are rejected the same way
the wrapper's [[ -f ... && -x ... ]] check does.
In `@Resources/bin/claude`:
- Around line 18-20: The self-reference guard currently compares paths as
strings using custom_real and self_real which fails for symlinked wrappers;
change the if condition that compares custom_real and self_real to use the
filesystem identity test (-ef) so the script detects "same file" even through
symlinks (i.e., replace the string comparison in the if [[ ... ]] block with a
test using -ef against custom_real and self_real and preserve the existing
behavior of exiting/avoiding recursion when they refer to the same file).
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: e3cc5d67-2b4e-439f-ab17-7e31df58ac3d
📒 Files selected for processing (3)
CLI/cmux.swiftResources/bin/claudeSources/cmuxApp.swift
🚧 Files skipped from review as they are similar to previous changes (1)
- Sources/cmuxApp.swift
- CLI: check isDirectory before accepting custom path - Wrapper: use bash -ef test instead of string path comparison to catch symlinked self-references
Summary
claudewrapper,cmux claude-teamsCLI, and UserDefaultsclaudeinstalled outside PATH or aliased in their shell profileTest plan
claudeandcmux claude-teamsresolve via PATH as beforeSummary by cubic
Added a “Claude Binary Path” setting so users can point to a specific
claudebinary when PATH or aliases don’t apply. It’s in its own settings card and is respected by theclaudewrapper,cmux claude-teams, and terminal sessions viaCMUX_CUSTOM_CLAUDE_PATH, with safe fallbacks.New Features
CMUX_CUSTOM_CLAUDE_PATHandUserDefaultskeyclaudeCodeCustomClaudePath; resolution: custom → PATH lookup → bundled binary.CMUX_CUSTOM_CLAUDE_PATHper session so shells use the chosen binary.Bug Fixes
-ef) to skip the wrapper itself.Written for commit 66d286e. Summary will update on new commits.
Summary by CodeRabbit
New Features
Localization