Repository navigation
feat: install the menu bar app without the Gatekeeper prompt - #96
Conversation
The app isn't signed with an Apple Developer ID, so a .dmg downloaded with a browser is quarantined and macOS shows "Apple could not verify…". Gatekeeper only checks files carrying com.apple.quarantine, which browsers add and curl / Node's fetch don't. - mac-cleaner-cli app install (alias app update): picks the build for the Mac (sysctl, so Rosetta doesn't hide Apple Silicon), downloads it from the latest release, verifies it against the SHA-256 digest GitHub publishes, extracts it with ditto (keeps the signature and symlinks), quits a running copy and swaps the bundle in from a staged copy so a failed copy never leaves the user without an app. Falls back to ~/Applications when /Applications isn't writable. --no-open and --dir options. - docs/install.sh, served by GitHub Pages, does the same for users without Node.js (curl, plutil, shasum, ditto; bash 3.2 compatible). - CI installs the app with both on macOS and checks the executable, the code signature and that the bundle isn't quarantined. - READMEs make the command the recommended install and explain the .dmg workaround and the limits of unsigned builds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QnDqAyRdicRYnwAAXydYoB
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 51b615c2d6
ℹ️ 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".
| echo "OK: $app" | ||
| } | ||
|
|
||
| node dist/index.mjs app install --no-open --dir "$RUNNER_TEMP/cli-apps" |
There was a problem hiding this comment.
Wait for desktop assets before testing the release
On release.published, this job and the Desktop App workflow start independently, while .github/workflows/desktop.yml:90-95 uploads the ZIPs only after building the app. If this command queries /releases/latest before that upload finishes, it selects the newly published release, finds no matching ZIP, and fails the test job; because publish has needs: test, this race blocks the npm release until CI is rerun. Add explicit workflow ordering or retry until the release assets are present.
Useful? React with 👍 / 👎.
| await rm(appPath, { recursive: true, force: true }); | ||
| await rename(stagedApp, appPath); |
There was a problem hiding this comment.
Preserve the installed bundle until the rename succeeds
When two installer invocations overlap, both use the fixed staging path from line 223; one can consume that path, after which the other deletes the installed bundle here and fails its rename with ENOENT, leaving no app installed. A process interruption or filesystem error between these two operations has the same result, and docs/install.sh:109-110 repeats the unsafe sequence. Keep the old bundle as a rollback target until the staged bundle has been committed successfully.
Useful? React with 👍 / 👎.
What does this PR do?
Adds two installers for the menu bar app. With either one, the app opens normally even though it isn't signed with an Apple Developer ID. Today a
.dmgdownloaded with a browser shows "Apple could not verify 'Mac Cleaner' is free of malware" (reported in #87).Why it works: Gatekeeper only checks files that carry the
com.apple.quarantineattribute. Browsers, Mail and AirDrop add it to downloads;curland Node'sfetchdon't. Nothing is bypassed on the user's Mac: the result is the same as building the app locally.mac-cleaner-cli app install(aliasapp update, options--no-openand--dir <folder>):sysctl, so a Node running under Rosetta still gets the Apple Silicon build);https://github.com/;ditto, which keeps the signature and symlinks, then copies it next to the destination and swaps it in with a rename. A failed copy never leaves the user without an app;~/Applicationswhen/Applicationsisn't writable, and opens the app.docs/install.shfor users without Node.js:curl -fsSL https://guhcostan.github.io/mac-cleaner-cli/install.sh | bash. It follows the same steps withcurl,plutil(reads the JSON),shasumandditto, and is bash 3.2 compatible (the macOS default). Everything runs frommainat the last line, so a truncated download never executes halfway. GitHub Pages already servesdocs/frommain.test (22)job installs the app from the latest release with both installers. It then checks the executable exists,codesign --verify --deep --strictpasses, and the bundle has no quarantine attribute..dmgworkaround and the limits of unsigned builds: Full Disk Access may need re-granting after an update, and there is no in-app auto-update.MAC_CLEANER_GITHUB_TOKENis optional and only lifts the API rate limit; CI uses it. I deliberately didn't useGITHUB_TOKEN, because a stale one exported in a developer's shell would turn every install into a 401.Type of change
Related issue
Follow-up to #87.
Checklist
bun run lintpassesbun run testpasses: 470 tests, coverage thresholds met (90.15% statements)bun run buildsucceedsapp installinstall.shran end to end on Linux with stubs for the macOS-only tools (plutil,ditto,sysctl…), using the realcurl;macos-latest.Notes for reviewer
npx mac-cleaner-cli app installonly works once a version with this command is published to npm, which is blocked on the expiredNPM_TOKEN. Thecurlinstaller works as soon as this merges.🤖 Generated with Claude Code
https://claude.ai/code/session_01QnDqAyRdicRYnwAAXydYoB
Generated by Claude Code