Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 28 additions & 5 deletions NORTH_STAR.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ it, local-first, reversible, and respectful of macOS permission boundaries.
- Preserve last-known permission posture for continuity and diagnostics while
keeping live macOS permission checks authoritative.
- Keep processing local. Do not add telemetry, trackers, cloud processing, or
hidden network dependency to the cursor loop.
hidden network dependency to the cursor loop. Update checks are allowed only
after the user explicitly enables internet access for update checks.

## Product Shape Today

Expand Down Expand Up @@ -50,6 +51,8 @@ Designer.
- If screen recording permission, helper installation, display state, or stored
settings are unavailable or invalid, the app fails safely and explains itself
through UI state or diagnostics instead of doing surprising work.
- If update checking is used, it is user-initiated from Settings and remains
off until the user explicitly allows internet access for that purpose.

## Pointer Capability Contract

Expand All @@ -72,6 +75,11 @@ Current supported contract:
must stay hidden or explicitly marked unavailable unless a public,
distribution-safe, tested implementation enables it through the app
capability model.
- AppKit cursor replacement is not a durable cross-application pointer
replacement guarantee. Other apps' cursor-update handling can reset it. The
current least-permission mitigation is a reapply supervisor; any stronger
persistence mechanism must come from a proven supervisor or helper capability
documented in `apps/macos/POINTER_PERSISTENCE_RESEARCH.md`.

Unsupported claims:

Expand All @@ -88,6 +96,8 @@ Prefer work that increases trust in the core utility:

- clearer identity and packaging consistency
- safer helper and XPC behavior only when a real pointer capability requires it
- a least-permission pointer supervisor that improves cross-app persistence
without private APIs, admin access, or broad Accessibility grants
- better crash recovery and orphan cleanup
- stronger display, color, and permission edge-case handling
- focused local verification for app, helper, settings, and packaging paths
Expand Down Expand Up @@ -116,11 +126,13 @@ true from live evidence:
6. Packaging scripts produce a validated app bundle and DMG from the repo-local
macOS package.
7. Signing, notarization, release metadata, and install instructions are
verified. Homebrew or cask distribution is optional and must remain absent
or explicitly blocked until its URL, checksum, notarization, and install
behavior are verified.
verified from a clean committed tree. Homebrew or cask distribution is optional
and must remain absent or explicitly blocked until its URL, checksum,
notarization, and install behavior are verified.
8. The repo contains no wrong-product surfaces, stale WindowDrop language,
telemetry, trackers, surprise network calls, or placeholder release claims.
9. Update checks are unavailable until the user enables internet access for
update checks, and any update action is user-initiated from Settings.

The phrase "mass production ready" means every item above has direct proof. A
green test suite alone is not enough if packaging, signing, notarization,
Expand All @@ -141,8 +153,12 @@ These are intentional blockers, not polish notes:
unavailable.
- Helper installation remains scaffolded and must not be sold as a user-facing
capability or required path until a supported pointer capability exists.
- Cursor persistence across other applications is not considered solved by
`NSCursor.set()` alone. If the app claims stronger cross-app persistence, it
must ship a verified least-permission supervisor or supported helper path.
- Release packaging, signing, notarization, stable release metadata, and DMG
install flow require live verification before any public launch claim.
install flow require live verification from a clean committed tree before any
public launch claim.
- Current release-authority blockers are tracked in GitHub issue #71:
https://github.com/rogu3bear/macOS-pointer-designer/issues/71. Keep that
issue open until the final North Star audit passes against the same
Expand Down Expand Up @@ -203,6 +219,11 @@ proof for another.
under assessment by identity, version, build, and executable digest; from
`apps/macos`, `make dmg-artifact-match-check`
- Signing identity: from `apps/macos`, `make signing-identity-check`
- Release source state: from `apps/macos`, `make release-source-state-check`;
public release gates must run from a clean committed tree so the signed app,
DMG, release metadata, and manual evidence all refer to the same source
state. `make release-readiness` also requires the app executable and DMG to
be newer than the commit being certified.
- Notarization credential setup and check: from `apps/macos`,
`make setup-notary-profile` with private operator `NOTARY_*` inputs, then
`make notary-profile-check`
Expand Down Expand Up @@ -232,5 +253,7 @@ explicit blocker.
- Making a helper mandatory for ordinary preference preview behavior.
- Adding telemetry, trackers, surprise network calls, or hidden background
services outside the documented app/helper model.
- Checking the internet for updates before the user enables update-check
internet access in Settings.
- Rebranding compatibility identifiers without a migration plan and full
identity verification.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ metadata, and completed manual release evidence before any mass-production
claim.

From `apps/macos`, the release authority lane starts with
`make setup-notary-profile` and `make notary-profile-check`, then proceeds
`make setup-notary-profile` and `make notary-profile-check`, verifies the
committed source state with `make release-source-state-check`, then proceeds
through `make release-candidate`, `make release-artifact-readiness`, and
`make release-readiness` against the same artifact.

Expand Down
2 changes: 1 addition & 1 deletion apps/macos/MANUAL_RELEASE_CHECKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ App executable SHA-256:

Machine gates:
- make release-readiness: Pass/fail
- spctl --assess --type open --verbose=4 CursorDesigner.dmg: Pass/fail
- spctl --assess --type open --context context:primary-signature --verbose=4 CursorDesigner.dmg: Pass/fail
- xcrun stapler validate CursorDesigner.dmg: Pass/fail

Manual observations:
Expand Down
6 changes: 5 additions & 1 deletion apps/macos/Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: all build release clean test install uninstall dmg create-dmg signed-dmg sign-dmg preflight launch-smoke dmg-install-check dmg-artifact-match-check release-metadata-check signing-identity-check setup-notary-profile notary-profile-check manual-release-evidence-template manual-release-evidence-check north-star-audit release-artifact-readiness release-readiness release-candidate
.PHONY: all build release clean test install uninstall dmg create-dmg signed-dmg sign-dmg preflight launch-smoke dmg-install-check dmg-artifact-match-check release-source-state-check release-metadata-check signing-identity-check setup-notary-profile notary-profile-check manual-release-evidence-template manual-release-evidence-check north-star-audit release-artifact-readiness release-readiness release-candidate

PRODUCT_NAME = CursorDesigner
BUILD_DIR = .build
Expand Down Expand Up @@ -105,6 +105,10 @@ dmg-install-check:
dmg-artifact-match-check:
@./Scripts/dmg-install-check.sh --app "$(APP_BUNDLE)" --dmg "$(DMG_NAME)" $(DMG_SIGNATURE_FLAGS)

# Verify public release gates are running from a committed tree.
release-source-state-check:
@./Scripts/release-source-state-check.sh

# Verify public release metadata is truthful for the current distribution state.
release-metadata-check:
@./Scripts/release-metadata-check.sh --app "$(APP_BUNDLE)" --repo "$(GITHUB_REPO)" --dmg "$(DMG_NAME)"
Expand Down
74 changes: 74 additions & 0 deletions apps/macos/POINTER_PERSISTENCE_RESEARCH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Cursor Persistence Research

Last reviewed: May 12, 2026.

This note explains why Cursor Designer's current custom cursor can return to the
default shape or orientation in some applications, and what a least-permission
fix should look like.

## Findings

1. `NSCursor.set()` is an AppKit cursor mechanism, not a durable system-wide
pointer replacement contract. Apple documents `NSCursor.set()` as making the
receiver the current cursor, and AppKit cursor rectangles/tracking areas are
designed so the view under the pointer can set I-beam, hand, resize, and
other cursors as the pointer crosses cursor regions.
2. The current implementation uses `NSCursor(image:hotSpot:)` and `cursor.set()`
in `CursorEngine.applyCursor()`. That can work while Cursor Designer or one
of its own cursor rects is controlling the pointer, but another application
can legitimately replace the cursor as part of normal AppKit cursor-update
handling.
3. A privileged helper is not automatically the right answer. The current helper
scaffold correctly keeps `supportsSystemWidePointerReplacement == false`
because a distribution-safe public API for replacing every system cursor has
not been proven here. Private WindowServer/CGS cursor replacement should stay
out of the release path unless a future tranche proves entitlement,
notarization, rollback, and user-consent behavior.
4. The least-permission durable path starts with a supervised pointer
presentation layer: a menu-bar agent that keeps the visible customization
alive by observing pointer motion and app/display changes, then reapplying or
drawing the custom pointer without installing a privileged helper. Mouse
event observation can use AppKit global monitors for mouse events; Apple
notes global monitors only observe events and cannot modify or prevent
delivery.
5. Screen Recording is only justified for dynamic contrast/background sampling.
It must not be requested for static color, static outline, settings, launch at
login, or update checks.
6. Accessibility is only justified when Cursor Designer needs to control or read
other applications through Accessibility APIs, monitor key-related events, or
synthesize input. It should not be requested just to check for updates or to
render the Preferences preview.
7. Update checks are network behavior and must be explicit. The app should only
contact release metadata when the user enables internet access for update
checks and presses a settings-menu update action. Future automatic updates
should use a signed appcast/updater system such as Sparkle only after the same
opt-in boundary is preserved.

## Recommended Architecture

Keep three separate capability layers:

- **AppKit preview layer**: no extra permission. Uses `NSCursor`, cursor rects,
and Preferences preview. This is useful but not a system-wide persistence
guarantee.
- **Pointer supervisor layer**: no privileged helper by default. Observes mouse,
active-app, display, sleep/wake, and settings changes; keeps the chosen pointer
presentation alive by reapplying the current cursor; degrades honestly in apps
or modes it cannot control. If it later uses a click-through overlay, it must
be reversible, local-only, and disabled before any password, secure input,
screen saver, or full-screen edge case that proves unsafe.
- **Privileged/helper layer**: disabled until proven. Only enable after there is
a public, notarizable, least-privilege mechanism with rollback, code-signing
verification, and manual permission evidence. Do not ship private CGS or SIP
workarounds as product behavior.

## Sources

- Apple Developer Documentation, `NSCursor.set()`: https://developer.apple.com/documentation/appkit/nscursor/set%28%29
- Apple Developer Documentation, `NSCursor`: https://developer.apple.com/documentation/appkit/nscursor
- Apple Cocoa Event Handling Guide, cursor-update events and cursor rectangles: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/EventOverview/MouseTrackingEvents/MouseTrackingEvents.html
- Apple Cocoa Event Handling Guide, tracking areas and `cursorUpdate:`: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/EventOverview/TrackingAreaObjects/TrackingAreaObjects.html
- Apple Developer Documentation, global event monitors: https://developer.apple.com/documentation/appkit/nsevent/addglobalmonitorforevents%28matching%3Ahandler:%29
- Apple Cocoa Event Handling Guide, global monitors cannot modify events and key monitoring needs Accessibility trust: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/EventOverview/MonitoringEvents/MonitoringEvents.html
- Apple Developer Documentation, macOS sandbox outgoing network entitlement: https://developer.apple.com/documentation/bundleresources/entitlements/com.apple.security.network.client
- Sparkle documentation, programmatic updater setup and user-controlled update checks: https://sparkle-project.org/documentation/programmatic-setup/
12 changes: 11 additions & 1 deletion apps/macos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ For the app-side production checklist, proof gates, and release blockers, see
- **Outline Mode**: Add a contrasting outline that adapts to the background
- **Menu Bar App**: Quick access to settings from the menu bar
- **Launch at Login**: Optionally start with your Mac
- **Explicit Update Checks**: Settings-gated release checks; no internet access until the user allows it
- **Pointer Scope Status**: Shows whether this build enables any broader pointer replacement capability
- **Multi-Monitor Support**: Handles different DPI scales and refresh rates per display
- **Crash Recovery**: Tracks app session state and recovers cleanly after unexpected termination
Expand Down Expand Up @@ -44,6 +45,7 @@ For release-candidate proof, use the repo gates instead of ad hoc notarization:
```bash
make setup-notary-profile NOTARY_PROFILE="<notarytool profile>"
make notary-profile-check NOTARY_PROFILE="<notarytool profile>"
make release-source-state-check
make release-candidate SIGN_IDENTITY="<Developer ID Application identity>" NOTARY_PROFILE="<notarytool profile>"
make release-artifact-readiness NOTARY_PROFILE="<notarytool profile>"
make release-readiness NOTARY_PROFILE="<notarytool profile>"
Expand Down Expand Up @@ -74,6 +76,7 @@ Click the cursor icon in the menu bar to:
- **Sampling Rate**: Background detection frequency (15-120 Hz)
- **Pointer Scope**: Shows whether broader pointer replacement is enabled in this build
- **Launch at Login**: Start automatically with macOS
- **Updates**: Allow internet access for update checks, then manually check verified release metadata

## Quick Start (Personal Use)

Expand Down Expand Up @@ -230,13 +233,20 @@ the signed, notarized release checklist before any stable download claim.

Cursor Designer requires:
- **Screen Recording**: To sample background colors (System Settings → Privacy & Security → Screen Recording)
- **Internet Access for Update Checks**: Off by default. The app only checks release metadata after you enable update-check internet access in Settings and press Check for Updates.
- **Administrator Access**: Not required for the current app behavior. Do not grant admin access unless a future release clearly enables and explains a supported helper capability.

The app may remember the last-known Screen Recording and Accessibility posture
for continuity and diagnostics, but live macOS permission checks always decide
what the app can do now.

No data is collected or transmitted. All processing happens locally.
No pointer data, settings, telemetry, or analytics are collected or transmitted.
All cursor processing happens locally. Update checks contact verified release
metadata only after explicit Settings consent.

The current AppKit cursor path is best-effort outside Cursor Designer's own
windows. For the research-backed persistence boundary and least-permission
supervisor direction, see [`POINTER_PERSISTENCE_RESEARCH.md`](POINTER_PERSISTENCE_RESEARCH.md).

## Troubleshooting

Expand Down
5 changes: 5 additions & 0 deletions apps/macos/RELEASE_RUNBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,16 @@ From `apps/macos`, run:

```bash
make signing-identity-check SIGN_IDENTITY="<Developer ID Application identity>"
make release-source-state-check
make release-candidate \
SIGN_IDENTITY="<Developer ID Application identity>" \
NOTARY_PROFILE="<notarytool profile>"
make release-readiness NOTARY_PROFILE="<notarytool profile>"
```

`make release-source-state-check` keeps public release proof tied to a clean
committed tree. In `make release-readiness`, that same guard also requires the
app executable and DMG to be newer than the commit being certified.
`make release-candidate` builds, signs, notarizes, staples, and runs artifact
readiness. `make release-readiness` additionally verifies stable GitHub release
metadata, so it must remain red until a stable GitHub release exists and its
Expand Down Expand Up @@ -148,6 +152,7 @@ passes on the same commit and DMG.
Stop and do not publish if any of these are true:

- Gatekeeper rejects the app or DMG.
- The release tree has uncommitted or untracked files.
- The DMG is not notarized or stapled.
- The stable GitHub release digest does not match the local DMG.
- Manual Screen Recording, persistence, drag-install, or local-first checks are
Expand Down
Loading
Loading