Repository navigation
fix(macos): never run Apple's placeholder git, which opens the developer tools window - #779
Merged
Merged
Conversation
…per tools window On a Mac without the developer tools, /usr/bin/git is a placeholder: running it, even as `git --version`, makes macOS open its "install the command line developer tools" window. The server asked "is git here?" exactly that way on the first page load (isGitAvailable() from renderIndexHtml), so a no-admin install on such a Mac ended with that window one second after the server started, and it would have come back at every start. Found on a new MacBook on 2026-11-06: the system log names the Codeman server as the parent of that git process. One gate now stands in front of every git the server starts (src/utils/git-availability.ts). On macOS it resolves git on the PATH itself; when that is the placeholder, `xcode-select -p` must name a developer folder that holds a git of its own. xcode-select only prints and never opens a window. Without the tools, git counts as absent and is never started: every caller gets the ENOENT it already answers with "git is not installed". A refusal is asked again after a minute, so installing the tools later needs no server restart for the git features, and isGitAvailable() no longer memoizes that one refusal. Gated: the clone and worktree runners, the Git panel's status and fetch, the update check and repository status, and Read My Mind's workspace signals. test/git-spawn-gate.test.ts covers the decision and scans src/ for a git spawn in a file that does not call the gate, with the reviewed list of sites pinned. Checked for real on macOS 27. With the developer tools present (Command Line Tools on one Mac, Xcode on another) the gate resolves /usr/bin/git, finds the tools and allows git, in 3 to 5 ms. On a MacBook with no developer tools, a fresh no-admin install with this fix started the server and loaded the page with git reported as absent, no window opened, and the system log holds no request to install the tools; the same install without the fix logged that request one second after the server started. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
On a Mac without the developer tools (no Xcode, no Command Line Tools),
/usr/bin/gitis not git. It is a placeholder from Apple: running it, even asgit --version, makes macOS open its "The git command requires the command line developer tools. Would you like to install the tools now?" window.The Codeman server ran exactly that. When a page is rendered, the server collects which tools exist on the machine, and for git it called
isGitAvailable(), which runsgit --version. So on a Mac without the developer tools:install.sh --no-admin, shipped in 1.49.0) finished cleanly, and one second after the server started, on the first page load, that system window appeared;The installer itself already avoids the placeholder for this reason. The server did not.
This PR puts one check in front of every place where the server starts git. It changes nothing on Linux, and nothing on a Mac that has the developer tools or another git (Homebrew, for example) first on the PATH.
How it was found
On a new MacBook (macOS 27.2, no developer tools, no Homebrew), installing Codeman without administrator rights. The install took about 25 seconds and reported no error. The system window opened right after. The system log names the Codeman server as the process that asked:
The fix
New module
src/utils/git-availability.ts, with the decision as pure functions and two small IO steps around it:gitthe server's PATH resolves to, without running it./usr/bin/git, askxcode-select -pwhether a developer folder is selected.xcode-selectis a real program that prints and exits; it never opens a window. The folder it names must also contain a git of its own (usr/bin/git, true for Xcode and for the Command Line Tools), so a selected Xcode that was deleted later does not count.ENOENTit would get on a machine with no git at all, which every caller already answers with "git is not installed".A "yes" is kept for the life of the process. A "no" is asked again after 60 seconds, so installing the developer tools later needs no server restart for the git features.
isGitAvailable()keeps its memo for a real answer but no longer memoizes this one refusal.Every git the server starts now goes through
gitSpawnAllowed(): cloning a repository as a case, git worktrees, the Git panel's status and fetch, the update check and repository status, and Read My Mind's workspace signals.Reviewing the diff
371 lines added, 1 removed, in 10 files. Most of it is two new files:
src/utils/git-availability.ts(new)gitSpawnWouldPrompt()is the decision;gitSpawnAllowed()is the memoized entry point.test/git-spawn-gate.test.ts(new)src/that fails when a file starts git without calling the gate. The reviewed list of call sites is pinned in the test, so a new one fails until someone looks at it.spawn('git')/execFile('git'). No other behaviour changes.src/git-clone.tsis the largest becauseisGitAvailable()'s comment grew.CLAUDE.mdHow it was checked
npm run typecheck,npm run lint, Prettier on the changed files: clean.npm test: 14684 passed, 1 failed, 12 skipped. The one failure wastest/frontend-public-tooling.test.tsin a fresh worktree that lacked the generated spreadsheet preview assets (npm installcreates them); with the assets in place it passes, and it does not touch this change./usr/bin/git, finds the tools and allows git, in 3 to 5 ms.Not in this PR