feat: brew cask + curl installer — no more "damaged" dialog (no Apple cert) - #11
Conversation
- scripts/install.sh: auditable one-line installer (curl | bash). Downloads the latest DMG from the version-agnostic /download, installs to /Applications, clears com.apple.quarantine from our own bundle so the unsigned alpha opens without the "damaged" dialog. No sudo, no global Gatekeeper changes; fails loudly on wrong arch / download failure / running app. - scripts/update-cask.sh: recompute the published DMG's sha256 and push the refreshed cask to the todddickerson/homebrew-funbutton tap. Idempotent; gh CLI for auth (no .env token needed). - scripts/sync-install-sh.sh: regenerate the web copy of install.sh so it never drifts from the source. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- app/install.sh/route.ts: serves scripts/install.sh verbatim at funbutton.ai/install.sh as text/plain (body imported from installer.json, generated from the source), so `curl -fsSL https://funbutton.ai/install.sh | bash` works and a human can read it in the browser first. - page.tsx: reorder the install section to brew (#1, recommended) -> curl (#2) -> manual .dmg (#3). The xattr steps are demoted to the manual path only; the hero's scary "damaged" callout is replaced with "one command, no warnings." Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SIGNING.md gains an interim-mitigation section (brew/curl clear quarantine now; Developer ID signing is still the permanent fix that also clears the manual .dmg path) and records the 4th occurrence. PROGRESS.md adds the work entry plus a durable ship checklist that includes "update the Homebrew tap." GAUNTLET-FINDINGS.md documents the corrected premise (brew does NOT strip quarantine by default) and the real root cause (broken ad-hoc signature seal). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reviewer's GuideAdds a Homebrew + curl based installation flow that explicitly clears macOS quarantine on FunButton.app, exposes the installer via a static Next.js route, and rewrites the landing page + docs to make brew/curl the primary, non-warning paths while demoting manual .dmg + xattr instructions. Sequence diagram for curl-based installer flow clearing quarantinesequenceDiagram
actor User
participant Bash
participant FunbuttonSite
participant macOS
User->>Bash: curl -fsSL https://funbutton.ai/install.sh | bash
Bash->>FunbuttonSite: GET /install.sh
FunbuttonSite-->>Bash: installer script (scripts/install.sh)
Bash->>Bash: uname -s / uname -m checks
alt not Darwin or not arm64
Bash-->>User: die "FunButton ships for Apple Silicon (arm64) only"
else Apple Silicon macOS
Bash->>macOS: pgrep -x funbutton
alt funbutton running
Bash-->>User: die "FunButton is currently running"
else not running
Bash->>FunbuttonSite: curl -fL -o FunButton.dmg GET /download
FunbuttonSite-->>Bash: FunButton.dmg
Bash->>macOS: hdiutil attach FunButton.dmg
macOS-->>Bash: mountpoint with FunButton.app
Bash->>macOS: ditto FunButton.app /Applications/FunButton.app
Bash->>macOS: xattr -dr com.apple.quarantine /Applications/FunButton.app
Bash->>macOS: open /Applications/FunButton.app
macOS-->>User: FunButton launches (no "damaged" dialog)
end
end
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- The new Bash helpers (
install.sh,update-cask.sh,sync-install-sh.sh) assume tools likecurl,hdiutil,ditto,xattr,shasum,git,gh, andnodeexist; consider adding early explicit checks with clear error messages so failures are easier to diagnose on partially configured machines. - The curl installer currently hardcodes
/Applicationsas the install target; if you expect power users or managed/macOS setups with nonstandard app locations, it might be worth allowing an override via an env var (e.g.FUNBUTTON_APP_DIR) while keeping/Applicationsas the default. scripts/update-cask.shpicks the .dmg asset using fairly loosegreppatterns and falls back to the first.dmg; tightening this selection (or failing loudly when multiple candidates are found) would make it safer against future changes in asset naming or layout.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The new Bash helpers (`install.sh`, `update-cask.sh`, `sync-install-sh.sh`) assume tools like `curl`, `hdiutil`, `ditto`, `xattr`, `shasum`, `git`, `gh`, and `node` exist; consider adding early explicit checks with clear error messages so failures are easier to diagnose on partially configured machines.
- The curl installer currently hardcodes `/Applications` as the install target; if you expect power users or managed/macOS setups with nonstandard app locations, it might be worth allowing an override via an env var (e.g. `FUNBUTTON_APP_DIR`) while keeping `/Applications` as the default.
- `scripts/update-cask.sh` picks the .dmg asset using fairly loose `grep` patterns and falls back to the first `.dmg`; tightening this selection (or failing loudly when multiple candidates are found) would make it safer against future changes in asset naming or layout.
## Individual Comments
### Comment 1
<location path="scripts/sync-install-sh.sh" line_range="22-24" />
<code_context>
+[ -f "$SRC" ] || { echo "!! $SRC not found" >&2; exit 1; }
+mkdir -p "$(dirname "$OUT")"
+
+node -e '
+ const fs = require("fs");
+ const [src, out] = process.argv.slice(1);
+ const script = fs.readFileSync(src, "utf8");
+ const next = JSON.stringify({ script }) + "\n";
</code_context>
<issue_to_address>
**issue (bug_risk):** Node inline script reads the wrong argv indices, so it will fail to read the intended files.
For `node -e`, `process.argv` is `[nodePath, '-e', scriptSource, SRC, OUT]`, so `slice(1)` yields `['-e', scriptSource, SRC, OUT]`. That makes `src = '-e'` and `out = scriptSource`, so `readFileSync` tries to read a file named `-e` and `existsSync` checks the inline script instead of `$OUT`.
Index from the correct position instead, for example:
```js
const [src, out] = process.argv.slice(3);
// or
const [,, , src, out] = process.argv;
```
This aligns `src` and `out` with the `$SRC` and `$OUT` shell args.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| node -e ' | ||
| const fs = require("fs"); | ||
| const [src, out] = process.argv.slice(1); |
There was a problem hiding this comment.
issue (bug_risk): Node inline script reads the wrong argv indices, so it will fail to read the intended files.
For node -e, process.argv is [nodePath, '-e', scriptSource, SRC, OUT], so slice(1) yields ['-e', scriptSource, SRC, OUT]. That makes src = '-e' and out = scriptSource, so readFileSync tries to read a file named -e and existsSync checks the inline script instead of $OUT.
Index from the correct position instead, for example:
const [src, out] = process.argv.slice(3);
// or
const [,, , src, out] = process.argv;This aligns src and out with the $SRC and $OUT shell args.
Kill the "damaged" dialog with no Apple Developer cert
The "FunButton is damaged and can't be opened" dialog has hit four separate times
(2026-08-02, 08-14, 08-15, and 08-17 on v0.1.8). This ships a certificate-free fix
so brew/curl users never see it again — and it's verified for real on the Mac
Studio, end to end.
The single most important proof
A brew-installed FunButton opens with no "damaged" dialog and no manual
xattr:From: https://github.com/todddickerson/homebrew-funbutton/blob/HEAD/Casks/funbutton.rbPremise corrected + real root cause
brew install --caskdoes NOT strip quarantine by default. On Homebrew6.0.17 (default opts) a plain cask install left
com.apple.quarantine(
0381;…;;…) on the app. So the cask strips it explicitly in apostflightthat touches only
FunButton.app; the curl installer does it inline. Neitherdisables Gatekeeper globally.
quarantine alone.
spctl/codesignreport "code has no resources butsignature indicates they must be present" (
Sealed Resources=none). Removequarantine (any path) → it launches. The two OSS competitors (freeflow,
unramble) ship plain casks that work only because their bundles are validly
signed; ours isn't.
What's here
This PR (funbutton repo):
scripts/install.sh— auditablecurl … | bashinstaller (no sudo, fails loudlyon wrong arch / download failure / running app).
scripts/update-cask.sh— recompute the published DMG's sha256 and push the tap.scripts/sync-install-sh.sh— keep the web copy of install.sh in lockstep.apps/web/app/install.sh/route.ts(+ generatedinstaller.json) — serves theinstaller at
funbutton.ai/install.shastext/plain.apps/web/app/page.tsx— install section reordered brew → curl → manual .dmg;xattrdemoted to the manual path; hero's "damaged" box replaced with "onecommand, no warnings."
SIGNING.md,PROGRESS.md(+ ship checklist),GAUNTLET-FINDINGS.md.Separate repo (already public):
todddickerson/homebrew-funbutton— the tap with
Casks/funbutton.rb.Verification (real evidence only — no faked screenshots)
brew install/uninstall/zapall verified;--zaptrashed app + models + cachesscripts/install.shrun end-to-end → installed, no quarantine, launched./install.shroute:next buildregisters it as a static route (○ /install.sh);served
content-type: text/plain; charset=utf-8and byte-identical toscripts/install.shon bothnext devand the productionnext start.itself sits behind Vercel's SSO deployment-protection wall (302 →
sso-api), soit can't be
curled without auth — prod (funbutton.ai) is public and will serve/install.shonce deployed. I did not deploy to prod.brew style: clean.brew audit --cask: clean (exit 0).brew audit --cask --new: 2 residual, both inherent to an unsigned alpha in a third-party tap —repo "not notable enough" (homebrew-core submission rule, N/A here) and "signature
verification failed" (needs Developer ID + notarization).
Gates
apps/webtsc + eslint: clean.apps/workertsc: clean.cargo fmt --check:clean. macOS-26 crash-guard grep: doc comments only.
Todd's turn
apps/webto prod sofunbutton.ai/install.shand the reorderedlanding go live (I only deployed a preview — see below — never prod).
which also clears the manual .dmg path.
🤖 Generated with Claude Code
Summary by Sourcery
Provide certificate-free Homebrew and curl installation paths that avoid macOS damaged-app warnings while keeping manual DMG installation and future code signing documented.
New Features:
Bug Fixes:
Enhancements:
Documentation:
Tests:
Chores: