Skip to content

feat: install the menu bar app without the Gatekeeper prompt - #96

Merged
guhcostan merged 1 commit into
mainfrom
claude/magical-davinci-m4n040
Oct 1, 2026
Merged

guhcostan merged 1 commit into
mainfrom
claude/magical-davinci-m4n040

Conversation

@guhcostan

Copy link
Copy Markdown
Owner

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 .dmg downloaded 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.quarantine attribute. Browsers, Mail and AirDrop add it to downloads; curl and Node's fetch don't. Nothing is bypassed on the user's Mac: the result is the same as building the app locally.

  • mac-cleaner-cli app install (alias app update, options --no-open and --dir <folder>):
    • picks the build for the Mac (via sysctl, so a Node running under Rosetta still gets the Apple Silicon build);
    • reads the latest release from the GitHub API and downloads the zip, which must come from https://github.com/;
    • verifies it against the SHA-256 digest GitHub publishes for the asset;
    • extracts it with 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;
    • quits a running copy first, falls back to ~/Applications when /Applications isn't writable, and opens the app.
  • docs/install.sh for users without Node.js: curl -fsSL https://guhcostan.github.io/mac-cleaner-cli/install.sh | bash. It follows the same steps with curl, plutil (reads the JSON), shasum and ditto, and is bash 3.2 compatible (the macOS default). Everything runs from main at the last line, so a truncated download never executes halfway. GitHub Pages already serves docs/ from main.
  • CI on a real Mac: the test (22) job installs the app from the latest release with both installers. It then checks the executable exists, codesign --verify --deep --strict passes, and the bundle has no quarantine attribute.
  • READMEs make the command the recommended install. They also explain the .dmg workaround 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_TOKEN is optional and only lifts the API rate limit; CI uses it. I deliberately didn't use GITHUB_TOKEN, because a stale one exported in a developer's shell would turn every install into a 401.

Type of change

  • Bug fix
  • New feature (new scanner, command, or option)
  • Improvement to existing feature
  • Chore / dependency update
  • Documentation

Related issue

Follow-up to #87.

Checklist

  • bun run lint passes
  • bun run test passes: 470 tests, coverage thresholds met (90.15% statements)
  • bun run build succeeds
  • New tests added for new behavior: 19 tests for app install
  • Tested against the real v1.4.0 release:
    • the CLI's download and checksum verification ran end to end;
    • install.sh ran end to end on Linux with stubs for the macOS-only tools (plutil, ditto, sysctl…), using the real curl;
    • cases covered: Apple Silicon, Intel, update while running, custom folder, tampered download (rejected, existing install kept), non-macOS.
  • Tested on a Mac by hand: the new CI step does it on macos-latest.

Notes for reviewer

npx mac-cleaner-cli app install only works once a version with this command is published to npm, which is blocked on the expired NPM_TOKEN. The curl installer works as soon as this merges.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QnDqAyRdicRYnwAAXydYoB


Generated by Claude Code

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
@guhcostan
guhcostan merged commit 35ff0ab into main Oct 1, 2026
8 checks passed

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread .github/workflows/ci.yml
echo "OK: $app"
}

node dist/index.mjs app install --no-open --dir "$RUNNER_TEMP/cli-apps"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment thread src/commands/app.ts
Comment on lines +228 to +229
await rm(appPath, { recursive: true, force: true });
await rename(stagedApp, appPath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

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.

2 participants