Skip to content
Back to skills

Release

CSecurity

Cut a new Claudesk release — bump the version, clean-build the .dmg + updater artifacts (.app.tar.gz + .sig + signed latest.json), publish a GitHub release, and bump the Homebrew tap cask. MANUAL-ONLY; invoke by hand when the operator says "cut a release" / "ship a release" / "/release". NEVER auto-invoked by feature-ship or product-finalize.

  • 9 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 19, 2026
developmentrustgoshellbashnodegitsecurity

Security analysis

C64/100
  • mediumUses curl or wget to download content
  • highPerforms destructive filesystem operations
  • criticalExfiltrates credentials via HTTP — exact pattern from Snyk ToxicSkills study
  • criticalSends environment variables or credentials to an external URL

Pro shows the line behind each finding and how to fix it

Scanned September 27, 2026

npx -y skills add StaymanHou/Claudesk --skill release --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Release?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Release
[![Security: C — Skills Directory](https://www.skillsdirectory.com/api/skills/staymanhou-release/badge)](https://www.skillsdirectory.com/skills/staymanhou-release)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: release
description: Cut a new Claudesk release — bump the version, clean-build the .dmg + updater artifacts (.app.tar.gz + .sig + signed latest.json), publish a GitHub release, and bump the Homebrew tap cask. MANUAL-ONLY; invoke by hand when the operator says "cut a release" / "ship a release" / "/release". NEVER auto-invoked by feature-ship or product-finalize.
---

# Release Claudesk

Drives a full Claudesk release end-to-end: bump the version, do a clean production
build, tag the commit, publish a GitHub release with **four assets** — the `.dmg`
(first-install / Homebrew) plus the three in-app-updater artifacts (`Claudesk.app.tar.gz`,
its minisign `.sig`, and the signed `latest.json` manifest) — and update the Homebrew
tap cask (`StaymanHou/homebrew-claudesk`) so `brew install --cask claudesk` picks up
the new version. The updater artifacts are what a running Claudesk downloads to
self-update (M10); the `.dmg`/cask path is unchanged.

The flow pauses at two human gates — before publishing the GitHub release, and
before pushing the tap cask — because both are irreversible outward-facing actions.

## ⚠️ MANUAL-ONLY — do not wire into any workflow

**This skill is invoked ONLY by the operator, by hand** (they say "cut a release",
"ship a release", or `/release`). It is deliberately a standalone skill with **no
transition wiring** into the feature or product state machines.

- **`feature-ship` MUST NOT invoke it.** Shipping a feature is a git push, not a
  product release.
- **`product-finalize` MUST NOT invoke it.** Closing a milestone/WBS cycle is a
  docs-and-archive operation, not a product release.
- A release is an **outward-facing publish** (cuts a public GitHub release + a
  binary friends will install). It happens on the operator's explicit say-so and
  never as a side effect of any close skill. If you are an orchestrator driving a
  workflow and you reach a "ship"/"finalize"/"close" step, do **not** reach for
  this skill — releasing is out of band.

If you find yourself about to call this skill from inside another skill's chain,
stop: that's the failure mode this constraint exists to prevent.

## Preconditions

- **Working tree clean**, on `main`. Uncommitted changes must be dealt with before
  starting (this skill will add a version-bump commit of its own).
- **`gh` installed and authenticated** (`gh auth status`). If missing:
  `brew install gh && gh auth login` (the auth is interactive — the operator runs it).
- **Apple Silicon host** — the build produces an `aarch64` `.dmg`.
- **Tap repo present** at `homebrew-claudesk/` **nested inside this project** (its own
  independent git repo; gitignored by the canonical claudesk repo). If absent, clone
  it there: `gh repo clone StaymanHou/homebrew-claudesk homebrew-claudesk`.
  Confirm its remote is SSH — `git -C homebrew-claudesk remote get-url origin` should
  print `git@github.com:StaymanHou/homebrew-claudesk.git`. If a fresh clone came down
  over HTTPS, switch it:

  ```bash
  git -C homebrew-claudesk remote set-url origin git@github.com:StaymanHou/homebrew-claudesk.git
  ```

- **Updater signing key present** at `~/.claudesk-release-keys/` (created M10 WP5):
  - `claudesk-release.key` — the minisign **private** key (`chmod 600`, NEVER committed).
  - `claudesk-release.key.pass` — its password (`chmod 600`, a random string; NEVER committed).
  - `claudesk-release.key.pub` — the public key, key ID `774E2E8429FDF78A`. Its base64 form
    is baked into `src-tauri/tauri.conf.json` → `plugins.updater.pubkey` — **the trust anchor
    every published update is verified against.** Do NOT change the config pubkey unless you are
    deliberately rotating the key (a key rotation strands users on the old key until they
    `brew upgrade`/re-download — see "first updatable release is the FLOOR" below).

  Confirm the key files are present before starting; if this machine doesn't have them, the
  build's sign step (step 3) cannot produce the `.sig` and the release cannot ship updater
  artifacts:

  ```bash
  ls -la ~/.claudesk-release-keys/claudesk-release.key ~/.claudesk-release-keys/claudesk-release.key.pass
  ```

  If missing (new machine / lost key): STOP. Either restore the key from your secret backup, or
  — if genuinely lost — mint a new one (`pnpm exec tauri signer generate -w ~/.claudesk-release-keys/claudesk-release.key`),
  swap the new `.pub` into `tauri.conf.json`, and accept that this becomes a key rotation.

- **Apple Developer ID signing identity + notarization credential present** (M14 WP2,
  2026-09-18). Claudesk is **signed and notarized** as of v0.5.2 — these are hard
  preconditions, not optional extras.

  ```bash
  security find-identity -v -p codesigning | grep "Developer ID Application"
  xcrun notarytool history --keychain-profile "claudesk-notary" >/dev/null && echo "notary OK"
  ```

  ⚠️ **Both must succeed before you start.** The first must print exactly one
  `Developer ID Application: …` line; the second is a real authenticated round-trip to
  Apple (it prints submission history, or "No submission history" on a fresh account —
  both are success; an auth failure is not).

  ⚠️ **`security find-identity -v` reporting `0 valid identities` while the cert IS in
  Keychain Access means Apple's G2 intermediate is missing**, not that the cert is bad.
  Symptom chain: red "certificate is not trusted" in Keychain Access → `codesign` fails
  with `unable to build chain to self-signed root` → `errSecInternalComponent`. Fix
  (idempotent, no sudo, login keychain only):

  ```bash
  curl -fsSL -o /tmp/DeveloperIDG2CA.cer https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer
  security import /tmp/DeveloperIDG2CA.cer -k ~/Library/Keychains/login.keychain-db
  ```

  ⚠️ **Do NOT validate the identity with `security verify-cert`** — it uses a different
  trust evaluation and returns "verification successful" in exactly the state where
  `codesign` cannot build the chain. A false all-clear. Test-sign a throwaway binary
  instead, or just trust `find-identity -v`.

## Inputs

- **Version** — e.g. `0.1.1`. Ask the operator if not given. Throughout these steps,
  let `VER` be the version (no leading `v`); the git tag is `vVER`.
  - In *prose and in the version-bump edits* (step 2), `VER`/`vVER` are textual
    placeholders you substitute by hand (`Claudesk_VER_aarch64.dmg` → `Claudesk_0.2.6_aarch64.dmg`).
  - In the *shell blocks from step 3 onward* (signing export, `latest.json` gen,
    `gh release create`, the endpoint check), `$VER`/`${VER}` are used as a **live shell
    variable**. **Export it once at the top of your release shell before running those
    blocks** so every `$VER` expands correctly (and so the same shell holds it across
    steps):

    ```bash
    export VER=0.2.6   # ← the version you're cutting, no leading v
    ```

    (Or derive it: `export VER="$(jq -r .version src-tauri/tauri.conf.json)"` **after**
    the step-2 bump commit, so it matches the source of truth.) The step-3+ blocks assume
    `$VER` is set; an unset `$VER` empty-expands (`Claudesk__aarch64.dmg`, a versionless
    `url`, an empty manifest `version`) — set it first.

## Steps

Run from the project root (`/Users/stayman/Personal/projects/claudesk`).

1. **Check preconditions** (above). Confirm `gh auth status` is OK, the working tree
   is clean, and the tap repo is present with an SSH remote. STOP and tell the
   operator if any fails.

2. **Bump the version.** Edit both:
   - `src-tauri/tauri.conf.json` → `version`
   - `src-tauri/Cargo.toml` → `[package] version`

   Then refresh `Cargo.lock` (so its `claudesk` entry matches the new version),
   commit, and push:

   ```bash
   (cd src-tauri && cargo update -p claudesk)   # rewrites the claudesk version in Cargo.lock; no network
   ./node_modules/.bin/prettier --write src-tauri/tauri.conf.json
   ./node_modules/.bin/prettier --check src-tauri/tauri.conf.json   # parse sanity only: right after --write it cannot catch a reflow
   git add src-tauri/tauri.conf.json src-tauri/Cargo.toml src-tauri/Cargo.lock
   git commit -m "Release vVER"
   git push origin main
   ```

   (`cargo update -p claudesk` just re-resolves the local package's own entry in the
   lockfile to the new `Cargo.toml` version — it does not bump dependencies. If for
   any reason `Cargo.lock` still shows the old version, the clean build in step 3
   will rewrite it; re-stage and amend if so. The release must point at a pushed
   commit.)

   ⚠️ **The Prettier pass is not optional.** Editing the version with a JSON tool (or a
   re-serializing editor) reflows `tauri.conf.json` — v0.6.0's release commit expanded the
   one-line `resources` array, which failed `format:check` and turned `pnpm verify:auto` red on
   `main` for every later WP. Only the version line should change in this file's diff.

3. **Export the updater-signing env vars, then clean build.** The build's integrated
   sign step produces the updater artifact **and its `.sig`** — but ONLY if the private
   key is supplied as a **string** in `TAURI_SIGNING_PRIVATE_KEY` (see the "Updater
   signing" gotcha below — `..._PATH` does NOT work for the build auto-sign path and
   ships an unsigned `.tar.gz`). Export both env vars in the **same shell** as the build:

   ```bash
   export TAURI_SIGNING_PRIVATE_KEY="$(cat ~/.claudesk-release-keys/claudesk-release.key)"
   export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$(cat ~/.claudesk-release-keys/claudesk-release.key.pass)"
   export APPLE_SIGNING_IDENTITY="Developer ID Application: Yuechen Hou (C8RJH77B47)"
   ```

   (The private key + its password live in `~/.claudesk-release-keys/` — see "Updater
   signing key" in Preconditions. Both are read as file CONTENTS via `$(cat …)`; never
   echo either value, never paste it into a committed file.)

   ⚠️ **`APPLE_SIGNING_IDENTITY` is deliberately an env var, not `tauri.conf.json`.**
   The tracked config carries only `bundle.macOS.hardenedRuntime` + `.entitlements`, so
   the repo stays machine-independent and `pnpm tauri:dev` keeps working for anyone
   without the cert. Setting `signingIdentity` in the config would break a contributor's
   dev build.

   ⚠️ **Notarization is a SEPARATE step (5c), deliberately not wired into the build.**
   Tauri *can* notarize during `tauri build` if `APPLE_ID` + `APPLE_PASSWORD` +
   `APPLE_TEAM_ID` are exported — but when they are **absent it prints
   `Warn skipping app notarization` and STILL EXITS 0**. That failure mode ships an
   un-notarized build that looks like a successful release. This skill therefore
   notarizes explicitly in step 5c and *verifies* in step 5d, where a miss is loud.

   Then a full cold rebuild guarantees no stale-cache artifact ships:

   ```bash
   (cd src-tauri && cargo clean)
   pnpm tauri build
   ```

   This is a **long** cold build (multi-minute — likely 3–8 min). Pass an explicit
   Bash `timeout` (consult `runtimes.md` for the recorded `pnpm tauri build` time;
   if absent, use 600000 ms — the Bash max — and record the observed time to
   `runtimes.md` afterward). Three artifacts land:
   - `src-tauri/target/release/bundle/dmg/Claudesk_VER_aarch64.dmg` (first-install / Homebrew)
   - `src-tauri/target/release/bundle/macos/Claudesk.app.tar.gz` (updater payload)
   - `src-tauri/target/release/bundle/macos/Claudesk.app.tar.gz.sig` (minisign signature)

   **Confirm all three exist — and specifically that the `.sig` is present:**

   ```bash
   ls -la src-tauri/target/release/bundle/dmg/Claudesk_${VER}_aarch64.dmg
   ls -la src-tauri/target/release/bundle/macos/Claudesk.app.tar.gz \
          src-tauri/target/release/bundle/macos/Claudesk.app.tar.gz.sig
   ```

   ⚠️ **If `Claudesk.app.tar.gz.sig` is MISSING**, the build did not sign (the env var
   wasn't exported as a string, or the password was wrong). STOP — do not publish an
   unsigned updater artifact (the app would reject the update at verify time). Re-export
   `TAURI_SIGNING_PRIVATE_KEY` (contents, not path) + the password and rebuild.

   **Confirm the build was Apple-signed** (this is separate from minisign):

   ```bash
   codesign -dv --verbose=2 src-tauri/target/release/bundle/macos/Claudesk.app 2>&1 \
     | grep -E "flags=|Authority=Developer ID Application"
   ```

   ⚠️ Expect `flags=0x10000(runtime)` **and** an `Authority=Developer ID Application: …`
   line. ⚠️ **Do NOT check `$?` here** — `codesign` **exits 0 even when signing failed**,
   leaving the previous signature in place. The `Authority=` lines are the evidence.
   If they are absent, `APPLE_SIGNING_IDENTITY` was not exported into the build's shell.

3b. **Notarize the `.dmg` and staple both artifacts.** ⚠️ **This step is why the release
   no longer needs an `xattr` workaround** — `spctl` only reports
   `source=Notarized Developer ID` once the ticket is stapled.

   ```bash
   xcrun notarytool submit src-tauri/target/release/bundle/dmg/Claudesk_${VER}_aarch64.dmg \
     --keychain-profile "claudesk-notary" --wait
   ```

   This uploads to Apple and blocks until a verdict (typically 1–5 min; it has taken
   longer). Pass an explicit Bash `timeout` of 600000 ms. The final line must read
   `status: Accepted`.

   ⚠️ **If the status is `Invalid`**, get the reason before changing anything —
   `xcrun notarytool log <submission-id> --keychain-profile "claudesk-notary"`. The
   usual causes are a missing hardened runtime or an unsigned nested binary, both of
   which are build-config problems, not notarization problems.

   Then staple the ticket to **both** artifacts:

   ```bash
   xcrun stapler staple src-tauri/target/release/bundle/dmg/Claudesk_${VER}_aarch64.dmg
   xcrun stapler staple src-tauri/target/release/bundle/macos/Claudesk.app
   ```

3c. **⚠️ RE-TAR AND RE-SIGN THE UPDATER PAYLOAD — do not skip this.** Tauri builds
   `Claudesk.app.tar.gz` **during** the build, i.e. **before** step 3b stapled the
   `.app`. The tarball on disk therefore contains an **unstapled** app, and its `.sig`
   was computed over those stale bytes. Shipping it means every self-updating user
   receives an app whose Gatekeeper check must go **online** to fetch the ticket — which
   fails offline and re-introduces exactly the first-launch friction this milestone
   deleted.

   ```bash
   (cd src-tauri/target/release/bundle/macos && tar -czf Claudesk.app.tar.gz Claudesk.app)
   ./node_modules/.bin/tauri signer sign \
     -k "$TAURI_SIGNING_PRIVATE_KEY" -p "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD" \
     src-tauri/target/release/bundle/macos/Claudesk.app.tar.gz
   ```

   ⚠️ **Order is load-bearing: staple → re-tar → re-sign.** Re-tarring invalidates the
   original `.sig` (different bytes), so the re-sign is mandatory, not optional. Verify
   the payload actually carries the ticket:

   ```bash
   rm -rf /tmp/tarcheck && mkdir -p /tmp/tarcheck
   tar -xzf src-tauri/target/release/bundle/macos/Claudesk.app.tar.gz -C /tmp/tarcheck
   xcrun stapler validate /tmp/tarcheck/Claudesk.app && rm -rf /tmp/tarcheck
   ```

   ⚠️ **`does not have a ticket stapled to it` means you skipped or mis-ordered 3c.**

3d. **Verify the release artifacts — all four checks, all required.** ⚠️ **Run this
   LAST, after every step that writes or moves a bundle.** A notarization ticket is
   carried in extended attributes, and several ordinary file operations silently drop
   them (see the `cp -R` warning below), so a verification that runs before the final
   file movement proves nothing about what ships.

   ```bash
   # 1-2. the .app: signed, and NOTARIZED (not merely signed)
   codesign --verify --deep --strict --verbose=2 src-tauri/target/release/bundle/macos/Claudesk.app
   spctl -a -t exec -vvv src-tauri/target/release/bundle/macos/Claudesk.app
   # 3. the .dmg (first-install / Homebrew path)
   xcrun stapler validate src-tauri/target/release/bundle/dmg/Claudesk_${VER}_aarch64.dmg
   # 4. ⚠️ THE UPDATER PAYLOAD — what self-updating users actually receive.
   #    Checks 1-3 all pass while this one fails if 3c was skipped or mis-ordered.
   rm -rf /tmp/payloadcheck && mkdir -p /tmp/payloadcheck
   tar -xzf src-tauri/target/release/bundle/macos/Claudesk.app.tar.gz -C /tmp/payloadcheck
   xcrun stapler validate /tmp/payloadcheck/Claudesk.app
   spctl -a -t exec -vvv /tmp/payloadcheck/Claudesk.app
   rm -rf /tmp/payloadcheck
   ```

   ⚠️ **`codesign` alone does not prove notarization.** The decisive line is `spctl`
   printing **`source=Notarized Developer ID`** — if it says `source=Developer ID` or
   **`source=Unnotarized Developer ID`**, the ticket is missing and users will hit
   Gatekeeper. STOP and redo 3b–3c rather than publishing.

   ⚠️ **Check 4 is the one that catches a broken updater path.** The `.app` and `.dmg`
   can both verify clean while the tarball carries an unstapled bundle — that is exactly
   the 3c failure mode, and it is invisible to checks 1–3 because they read a *different
   artifact*. The tarball is the only one a self-updating user ever sees.

   ⚠️ **`cp -R` STRIPS THE NOTARIZATION TICKET.** Verified 2026-09-18: copying a stapled
   `.app` with `cp -R` yields `spctl → rejected, source=Unnotarized Developer ID`. Extended
   attributes do not survive a plain copy. If you ever need to move a stapled bundle, use
   `ditto` (which preserves xattrs) or re-staple afterwards — and re-run this step. The
   updater's own install path is safe: `tauri-plugin-updater` extracts via
   `tar::Archive::unpack` + `rename`, and tar *does* restore xattrs (which is why 3c's
   re-tar works).

4. **Compute the SHA-256** (the cask needs it; keep the hash):

   ```bash
   shasum -a 256 src-tauri/target/release/bundle/dmg/Claudesk_${VER}_aarch64.dmg
   ```

5. **Draft the release notes from the CHANGELOG.** Read `CHANGELOG.md` and collect
   the entries added since the previous release tag (`git tag --sort=-v:refname` →
   the tag before `vVER`; `git log <prevtag>..HEAD` to see what landed). Draft a
   short release-notes body summarizing those entries, and **always append the
   standard install block**:

   ```
   Install via the tap: `brew tap StaymanHou/claudesk && brew trust --cask StaymanHou/claudesk/claudesk && brew install --cask claudesk`
   ```

   ⚠️ **The unsigned-build caveat is GONE as of v0.5.2** (M14 WP2). Builds are
   Developer-ID signed and Apple-notarized, so Gatekeeper admits them unaided.
   **Do not re-add an `xattr -dr com.apple.quarantine` line to release notes** — it
   would instruct users to work around a problem that no longer exists, and it teaches
   a habit that weakens Gatekeeper for unrelated software.

5b. **Generate the updater manifest `latest.json`.** This is the file the running app
   polls (`plugins.updater.endpoints` in `tauri.conf.json` →
   `https://github.com/StaymanHou/Claudesk/releases/latest/download/latest.json`). Compose
   it from data already in hand — `VER`, the `.sig` contents, and the eventual asset URL:

   ```bash
   SIG="$(cat src-tauri/target/release/bundle/macos/Claudesk.app.tar.gz.sig)"
   PUB_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
   NOTES="<one-line summary — reuse the release-notes headline from step 5>"   # ← SUBSTITUTE before running
   jq -n \
     --arg version "$VER" \
     --arg notes "$NOTES" \
     --arg pub_date "$PUB_DATE" \
     --arg sig "$SIG" \
     --arg url "https://github.com/StaymanHou/Claudesk/releases/download/v${VER}/Claudesk.app.tar.gz" \
     '{version:$version, notes:$notes, pub_date:$pub_date,
       platforms:{"darwin-aarch64":{signature:$sig, url:$url}}}' \
     > latest.json
   ```

   ⚠️ **Substitute the `NOTES=` placeholder** with the real headline before running —
   unlike `signature`/`url`, `notes` has no downstream sanity-check, so an un-substituted
   `<one-line summary…>` would publish verbatim into the manifest. The GATE-1
   `cat latest.json` review is the backstop; substitute it here so the gate confirms, not fixes.

   ⚠️ **`signature` = the `.sig` file contents VERBATIM.** The `.sig` tauri produces is
   ALREADY base64; the updater plugin base64-**decodes** the field once at verify time.
   **Do NOT re-encode it** (`base64 latest… ` / `openssl base64` on the `.sig` → double-encoded
   → `Signature::decode` fails → every update rejects). The `--arg sig "$SIG"` above passes
   it as-is — correct. (Empirically confirmed: verbatim `.sig` verifies; double-encoded fails.)

   Sanity-check the manifest before the gate:

   ```bash
   jq -e '.version and .platforms."darwin-aarch64".signature and .platforms."darwin-aarch64".url' latest.json
   # signature field content must equal the .sig file content. Compare via $(...) on BOTH sides
   # so the .sig's trailing newline (which jq strips from the stored field) doesn't false-alarm —
   # a plain `diff` reports "\ No newline at end of file" even when the base64 is identical.
   [ "$(jq -r '.platforms."darwin-aarch64".signature' latest.json)" = \
     "$(cat src-tauri/target/release/bundle/macos/Claudesk.app.tar.gz.sig)" ] \
     && echo "sig verbatim OK" || echo "⚠️ signature field != .sig content — do NOT publish"
   ```

6. **⏸ GATE 1 — confirm before publishing.** Show the operator:
   - the built `.dmg` path + its size + the sha256,
   - the four assets that will upload (dmg + `Claudesk.app.tar.gz` + `.sig` + `latest.json`),
   - the composed `latest.json` (`cat latest.json`) — confirm `version`, `url`, and that
     `signature` is the verbatim `.sig`, and
   - the full drafted release notes.

   Wait for explicit approval (and apply any edits the operator gives). Do NOT run
   step 7 until they say go — step 7 publishes publicly.

7. **Tag + publish the GitHub release — with ALL FOUR assets.** The `.dmg`
   (first-install/Homebrew) **plus** the three updater artifacts (`Claudesk.app.tar.gz`,
   its `.sig`, and `latest.json`) go up in one `gh release create`:

   ```bash
   git tag "v${VER}"
   git push origin "v${VER}"
   gh release create "v${VER}" \
     "src-tauri/target/release/bundle/dmg/Claudesk_${VER}_aarch64.dmg" \
     "src-tauri/target/release/bundle/macos/Claudesk.app.tar.gz" \
     "src-tauri/target/release/bundle/macos/Claudesk.app.tar.gz.sig" \
     "latest.json" \
     --title "Claudesk v${VER}" \
     --notes "<the approved notes from step 6>"
   ```

   Confirm all four asset names are present:

   ```bash
   gh release view "v${VER}" --json assets --jq '.assets[].name'
   # expect: Claudesk_<VER>_aarch64.dmg, Claudesk.app.tar.gz, Claudesk.app.tar.gz.sig, latest.json
   ```

   The dmg asset URL will be
   `https://github.com/StaymanHou/Claudesk/releases/download/v<VER>/Claudesk_<VER>_aarch64.dmg`.

   **Then confirm the updater ENDPOINT resolves** — the exact URL the running app polls
   (`releases/latest/download/latest.json`, which GitHub aliases to *this* release once
   it's the latest). Fetch it and confirm it's the manifest you just published:

   ```bash
   curl -sSL https://github.com/StaymanHou/Claudesk/releases/latest/download/latest.json | jq -e '.version == "'"$VER"'"' \
     && echo "endpoint resolves → version $VER" \
     || echo "⚠️ endpoint did NOT return version $VER — the app's check() will not see this update"
   ```

   (If this is a pre-release or not the newest tag, `latest/download/` points elsewhere —
   the check is meaningful only when vVER is the newest published release, which is the
   normal `/release` case.)

8. **Bump the tap cask.** In `homebrew-claudesk/Casks/claudesk.rb` (nested in this
   project), update:
   - `version "VER"`
   - `sha256 "<hash from step 4>"`

   The `url` is version-interpolated (`v#{version}` / `#{version}`), so it does not
   need editing. Validate against the installed tap copy (audit/style read the
   INSTALLED copy, not the working tree):

   ```bash
   cp homebrew-claudesk/Casks/claudesk.rb \
      "$(brew --repository)/Library/Taps/staymanhou/homebrew-claudesk/Casks/claudesk.rb"
   brew audit --cask staymanhou/claudesk/claudesk    # must be clean (no output)
   brew style staymanhou/claudesk/claudesk           # must be clean
   ```

   If audit or style fails, fix the cask and re-validate before the gate.

9. **⏸ GATE 2 — confirm before pushing the tap.** Show the operator the cask diff
   (`git -C homebrew-claudesk diff Casks/claudesk.rb`) and the clean audit/style
   results. Wait for explicit approval before step 10.

10. **Push the tap:**

    ```bash
    git -C homebrew-claudesk commit -am "claudesk VER"
    git -C homebrew-claudesk push origin HEAD
    ```

11. **Clean up the transient manifest, then report.** Remove the repo-root
    `latest.json` (it's already uploaded to the release; leaving it dirties the working
    tree and would trip the clean-tree precondition on the next run — it's also
    gitignored as a backstop):

    ```bash
    rm -f latest.json
    ```

    Then **report** to the operator: the release URL, the tap cask commit, and the
    one-paste install command. Record the build time in `runtimes.md` if it was a
    fresh measurement.

## Notes & gotchas (learned 2026-06-24 on the v0.1.0 cut)

- **Homebrew 6.x removed `--no-quarantine`.** Do NOT put
  `brew install --cask --no-quarantine claudesk` in release notes or the README — it
  errors with _"invalid option: --no-quarantine"_. ⚠️ **The flag is still gone, but the
  reason it mattered is not:** as of v0.5.2 the build is notarized, so plain
  `brew install --cask claudesk` just works with **no follow-up command at all**.
  (Signing + notarization
  will remove this step entirely — a deferred M9 upgrade.)
- **Third-party-tap trust gate.** Recent Homebrew refuses casks from untrusted
  taps. Friends run `brew trust --cask StaymanHou/claudesk/claudesk` once. Keep
  this in the tap README's install block.
- **Cask `verified:` param.** Don't add `verified:` to the `url` when the url
  domain == homepage domain (both `github.com`) — `brew audit` rejects it as
  unnecessary.
- **`brew audit`/`brew style` read the INSTALLED tap copy**, not your working tree.
  Always `cp` your edited `claudesk.rb` into
  `$(brew --repository)/Library/Taps/staymanhou/homebrew-claudesk/Casks/` before
  auditing, or push first and `brew update`.
- **Two repos, two pushes.** The app code/tag/release goes to `StaymanHou/Claudesk`;
  the cask bump goes to `StaymanHou/homebrew-claudesk` via `git -C homebrew-claudesk`.
  They are independent repos nested on disk (the tap is gitignored by claudesk) — a
  push to one never touches the other.
- **⚠️ SIGNED + NOTARIZED as of v0.5.2 — this REVERSES the M10 decision (M14 WP2,
  2026-09-18).** Builds are Developer-ID signed with the hardened runtime and
  Apple-notarized, so `spctl` reports `source=Notarized Developer ID` and Gatekeeper
  admits them unaided. **The `xattr` step is deleted everywhere** — from release notes
  and the README — and the updater's post-install
  self-quarantine-clear was removed with it. ⚠️ **minisign is RETAINED, not replaced:**
  the two systems are independent (minisign verifies the *updater payload* inside
  `download()`; notarization satisfies *Gatekeeper* via a stapled ticket), and the trust
  anchor `774E2E8429FDF78A` is **unchanged** — changing it would strand every existing
  install. Apple-Silicon-only is unchanged. See
  `workflow-system/product/arch/build-update-release.md`.
  *(Supersedes both the earlier "No `tauri-plugin-updater` yet — friends re-`brew upgrade`"
  note (the updater shipped at M10 WP2) and the M10 "stay unsigned + minisign" lock.)*
- **Updater signing (M10 WP5) — two gotchas that silently break updates if missed.**
  1. **`TAURI_SIGNING_PRIVATE_KEY` is the key CONTENTS (a string), NOT a path.** The build's
     integrated sign step reads `TAURI_SIGNING_PRIVATE_KEY` (+ `..._PASSWORD`). If you set
     only `TAURI_SIGNING_PRIVATE_KEY_PATH`, the build **completes bundling then exits 1** with
     _"A public key has been found, but no private key…"_ and ships an **unsigned** `.tar.gz`
     (no `.sig`). Always `export TAURI_SIGNING_PRIVATE_KEY="$(cat ~/.claudesk-release-keys/claudesk-release.key)"`
     (step 3). The `.sig`-present guard in step 3 catches a missed export.
  2. **`latest.json` `signature` = the `.sig` contents VERBATIM — do NOT re-base64-encode.**
     Tauri's `.sig` is already base64; the updater plugin base64-**decodes** the field once at
     verify time. Re-encoding → `Signature::decode` fails → every update is rejected. Step 5b's
     `--arg sig "$SIG"` passes it as-is; the `diff` sanity-check confirms byte-identity.
- **First updatable release is the FLOOR, not retroactive.** The in-app updater only updates
  *from* a build that already contains it (M10 WP2 = the first). The first `/release` cut after
  M10 ships is the floor: it publishes the updater artifacts, but users on older builds (v0.2.x)
  must `brew upgrade` / re-download **once** to land on an updater-capable version — thereafter
  they self-update. A **key rotation** (changing `plugins.updater.pubkey`) resets this floor:
  builds signed with the old key can't verify a new-key update, so a rotation also forces a
  one-time `brew upgrade`/re-download. Keep the key (`774E2E8429FDF78A`) stable across releases.
- **Homebrew installs never self-update — detect-and-defer (M10 WP3).** A brew-cask install of
  Claudesk resolves to a `/Caskroom/` path; the updater detects this and **defers to `brew
  upgrade`** rather than self-installing (avoids brew version desync). So the tap-cask bump
  (steps 8–10) remains the update path for brew users; the in-app updater serves direct-download
  installs only. Nothing to do here beyond the normal cask bump — noted so the two paths aren't
  conflated.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…