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
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ body:
id: proposal
attributes:
label: Proposed behavior
description: Explain how it would fit a small, offline Mac cleaning utility.
description: Explain how it would fit a small, private Mac cleaning utility.
validations:
required: true
- type: textarea
Expand Down
2 changes: 1 addition & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ List the checks you actually ran and any remaining limitations. For input-filter

## Contribution checklist

- [ ] The change is focused and preserves the offline and input-safety boundaries.
- [ ] The change is focused and preserves the privacy and input-safety boundaries, with no network use beyond update checks.
- [ ] Relevant tests or content checks pass, or limitations are explained above.
- [ ] No credentials, personal diagnostics, or generated app bundles are included.
- [ ] I have the right to contribute this work under Apache 2.0; any third-party material is identified.
10 changes: 7 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Keydoze
Native macOS 26+ SwiftUI/AppKit utility; Swift 6, no package dependencies or network access.
Native macOS 26+ SwiftUI/AppKit utility; Swift 6. Sparkle 2 is the only package dependency, and update checks and downloads to GitHub are the only network use (PRIVACY.md).

- Build and launch: `./script/build_and_run.sh`
- Build only: `./script/build_and_run.sh --build`
Expand All @@ -10,14 +10,18 @@ Native macOS 26+ SwiftUI/AppKit utility; Swift 6, no package dependencies or net
- Live filtering check (debug only, 8-second cap): arm `python3 script/test_watchdog.py --seconds 30 --activate` and wait for ARMED, then start a session in an app launched with `--live-check --evidence /abs/path`. Details and fault flags in [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md).
- Icon: `./script/build_icon.sh` regenerates `Resources/Keydoze.icns` from `Resources/Brand/Keydoze-source.png`.

Code map: `Sources/KeydozeCore/Session.swift` (deterministic phases, deadlines, recovery; no AppKit), `Sources/Keydoze/InputEngine.swift` (event tap, run loop thread, watchdog), `SessionModel.swift` (presentation, preferences, permission, lifecycle), `MainView.swift` (UI). `Debug*.swift`, the Preview menu, and every `--flag` are inside `#if DEBUG` and absent from release builds. Tests: `KeydozeCoreTests` (pure) and `KeydozeAdapterTests` (builds real CGEvents in memory; needs a logged-in GUI session for WindowServer, so run outside a headless sandbox).
Code map: `Sources/KeydozeCore/Session.swift` (deterministic phases, deadlines, recovery; no AppKit), `Sources/Keydoze/InputEngine.swift` (event tap, run loop thread, watchdog), `SessionModel.swift` (presentation, preferences, permission, lifecycle), `MainView.swift` (UI), `Updates.swift` (Sparkle and its session guard). `Debug*.swift`, the Preview menu, and every `--flag` are inside `#if DEBUG` and absent from release builds. Tests: `KeydozeCoreTests` (pure) and `KeydozeAdapterTests` (builds real CGEvents in memory; needs a logged-in GUI session for WindowServer, so run outside a headless sandbox).

Permission identity: the bundle ID `app.cleanmode.mac` and a consistent Apple Development signing identity are what keep the Accessibility grant across rebuilds. Do not rename the ID. The build script uses the first `Apple Development:` identity or `KEYDOZE_SIGN_IDENTITY`; ad-hoc signing loses the grant on every rebuild.

Releases: version and build live only in `Resources/Info.plist` and are bumped by hand; also update CHANGELOG.md and the README download name. Distribution is a separate Developer ID copy (team 7BVMD7TA97), notarized with `asc` from the keychain, never the development bundle; follow [docs/RELEASING.md](docs/RELEASING.md). 0.1.0 shipped 2026-10-21 as a notarized DMG and ZIP. Evidence and limits are in [docs/VALIDATION.md](docs/VALIDATION.md).
Updates: Sparkle's standard updater and UI, from `UpdateController`. While a session runs, no check starts, Sparkle's windows stay hidden, its alerts are dismissed, and a relaunch waits; keep all of that when changing either side. Feed: `releases/latest/download/appcast.xml`, so every release must attach `appcast.xml`. Sparkle compares `CFBundleVersion`, so the build number must rise every release. The private EdDSA key is only in the login Keychain under account `keydoze`, read by `generate_appcast`; never print or copy it, and `SUPublicEDKey` must match it. Lose it and every installed copy needs a download by hand. Debug builds never touch the real feed: the updater starts only with `--update-feed URL`, and flags that take a value must come before bare flags or AppKit opens no main window. Sparkle writes `SU*` keys to the real `app.cleanmode.mac` defaults even from a test copy, so remove them after a test. Details in [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md).

Releases: version and build live only in `Resources/Info.plist` and are bumped by hand; also update CHANGELOG.md and the README download name. `script/make_release.sh <version>` builds the separate Developer ID copy (team 7BVMD7TA97), notarized with `asc` from the keychain, never the development bundle, plus the DMG, ZIP, appcast and checksums; follow [docs/RELEASING.md](docs/RELEASING.md). Evidence and limits are in [docs/VALIDATION.md](docs/VALIDATION.md).

New GitHub issues fire the "Bug report to draft PR" routine through `.github/workflows/issue-fix.yml` (3 per day; the `fix-this` label re-fires it).

The website is `keydoze/` in dlev02/Portfolio (keydoze.drewlevinson.me); keep its privacy and download copy in step with this repository.

The engine owns event filtering and deadlines off the main thread. The deterministic core is in KeydozeCore. Never log typed content or raw events, replay blocked events, automatically restart an interrupted session, or request extra privacy permissions by habit. Idle means no event taps.

Live filtering tests require a short duration and an independent external process-kill watchdog armed before activation. UI simulation never creates a tap. Do not test TCC revocation, sleep, or session switching during live filtering on the user's primary Mac; use an isolated test account/device. Hardware gestures, Fn/media keys, Touch ID, and power/security paths must never be advertised as comprehensively blocked without evidence. Preserve unrelated workspace changes. Distribution signing, notarization, and publication require separately authorized credentials/targets.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## 0.2.0 — 5 November 2026

- Keydoze now updates itself. It checks once a day and asks before installing, and every update is signed. Turn automatic checks off in Keydoze > Settings…, or check any time with Keydoze > Check for Updates….
- Updates stay out of the way of a cleaning session: no check starts, no window appears, and no relaunch happens until input is back.
- Update checks and downloads go to GitHub. See the privacy policy for what GitHub can see.
- About now links to the privacy policy.

Coming from 0.1.0? It has no updater, so this is the one download you need to install by hand.

## 0.1.0 — 21 October 2026

- First notarized public release: Developer ID signed, accepted by Apple notarization, and stapled for Apple silicon Macs on macOS 26+.
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Bug reports, focused fixes, documentation improvements, and careful accessibilit

## Development

Use macOS 26 or later and Xcode 26 or later with Swift 6.2 or newer. The project has no package dependencies. Read [AGENTS.md](AGENTS.md) for its safety invariants and local commands.
Use macOS 26 or later and Xcode 26 or later with Swift 6.2 or newer. Sparkle, for updates, is the only package dependency. Read [AGENTS.md](AGENTS.md) for its safety invariants and local commands.

```sh
swift test --disable-sandbox
Expand All @@ -20,7 +20,7 @@ Live filtering checks are separate from automated tests. Use a short session and

Keep a change focused and preserve unrelated work. Describe the user-visible result, relevant checks, and anything you could not verify. Add regression coverage for meaningful behavior changes; content and link checks are usually enough for documentation. Do not include signing keys, credentials, local diagnostics, personal paths, or generated app bundles.

Keydoze must remain offline and must never store typed content, replay suppressed input, automatically restart an interrupted session, or leave an event tap installed while idle. Changes to those boundaries need explicit discussion.
Keydoze must not use the network for anything but update checks and downloads, and must never store typed content, replay suppressed input, automatically restart an interrupted session, or leave an event tap installed while idle. Changes to those boundaries need explicit discussion.

## License and attribution

Expand Down
3 changes: 3 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ Copyright 2026 Drew Levinson
Original project: https://github.com/dlev02/Keydoze

Licensed under the Apache License, Version 2.0. See LICENSE.

This product includes Sparkle (https://sparkle-project.org), under the MIT-style
license and third-party notices in Sparkle-LICENSE.txt.
21 changes: 19 additions & 2 deletions PRIVACY.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
# Privacy

Keydoze works offline. It has no accounts, advertising, analytics, remote logging, update checker, or network requests. The app does not collect or send personal data to its developer or any third party.
Last updated: 5 November 2026

Keydoze has no accounts, advertising, analytics, or remote logging. Cleaning sessions work without a network connection. The only network requests the app makes are update checks and update downloads, both to GitHub. Nothing is sent to the developer.

## Update checks

Keydoze uses [Sparkle](https://sparkle-project.org), an open-source update framework, to keep itself current.

- **When:** when you open Keydoze if the last check was more than a day ago, then once a day while it stays open. No check starts during a cleaning session.
- **What it requests:** one file, `appcast.xml`, from Keydoze's [GitHub releases](https://github.com/dlev02/Keydoze/releases). It downloads an update only after you choose **Install Update**.
- **What GitHub can see:** as with any web request, your IP address and the time, plus the app's name and version and Sparkle's version (for example `Keydoze/0.2.0 Sparkle/2.10.1`) and your Mac's language preference. GitHub handles this under [its privacy statement](https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement).
- **What is not sent:** typed input, input events, your session history, device identifiers, or a hardware or system profile.
- **What the developer can see:** GitHub shows repository owners a running total of downloads for each release file. Checks add to the total for `appcast.xml`. It does not show who made them.
- **What stays on your Mac:** the time of the last check, your choices about updates, and a downloaded update until it is installed. These are in Keydoze's local preferences and caches.

To turn automatic checks off, choose **Keydoze > Settings…** and turn off **Check for updates automatically**. Keydoze then makes no network requests unless you choose **Keydoze > Check for Updates…**.

Versions before 0.2.0 have no update checker and make no network requests.

## Input processing

Expand All @@ -10,7 +27,7 @@ The event tap is removed when the session ends or is interrupted. There is no in

## What stays on your Mac

The app saves the selected input scope and session duration in local macOS preferences. It does not sync those preferences or upload them. macOS manages the permission that allows Keydoze to intercept input; granting or revoking that permission happens in System Settings.
The app saves the selected input scope, session duration, and update settings in local macOS preferences. It does not sync those preferences or upload them. macOS manages the permission that allows Keydoze to intercept input; granting or revoking that permission happens in System Settings.

Development builds include optional diagnostics that are excluded from release builds:

Expand Down
15 changes: 15 additions & 0 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ let package = Package(
name: "Keydoze",
platforms: [.macOS("26.0")],
products: [.executable(name: "Keydoze", targets: ["Keydoze"])],
dependencies: [.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.0.0")],
targets: [
.target(name: "KeydozeCore"),
.executableTarget(name: "Keydoze", dependencies: ["KeydozeCore"]),
.executableTarget(name: "Keydoze", dependencies: ["KeydozeCore", .product(name: "Sparkle", package: "Sparkle")]),
.testTarget(name: "KeydozeCoreTests", dependencies: ["KeydozeCore"]),
.testTarget(name: "KeydozeAdapterTests", dependencies: ["Keydoze", "KeydozeCore"])
]
Expand Down
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@ A small native macOS utility built with SwiftUI and AppKit. Choose what to pause
- **Short sessions:** 30 seconds, 1 minute, or 2 minutes, with time to lift your hands before input pauses.
- **Always-visible recovery:** hold both Shift keys for two seconds, or press Escape in pointing-only mode.
- **Made for Mac:** a continuous native window, light and dark appearances, subtle SwiftUI transitions, and accessible motion and transparency fallbacks.
- **Completely offline:** no accounts, analytics, advertising, network requests, or saved input.
- **Private:** no accounts, analytics, advertising, or saved input. Sessions work offline; only update checks use the network, and you can turn them off.

## Get started

Download the notarized **Keydoze-0.1.0.dmg** from the [latest release](https://github.com/dlev02/Keydoze/releases/latest), open it, and drag **Keydoze** onto **Applications**. It is Developer ID signed, notarized by Apple, and runs on Apple silicon Macs with **macOS 26+**. A ZIP of the same app is also attached. To build from source instead, you need an Xcode toolchain supporting **Swift 6.2+**:
Download the notarized **Keydoze-0.2.0.dmg** from the [latest release](https://github.com/dlev02/Keydoze/releases/latest), open it, and drag **Keydoze** onto **Applications**. It is Developer ID signed, notarized by Apple, and runs on Apple silicon Macs with **macOS 26+**. A ZIP of the same app is also attached.

Keydoze keeps itself up to date from there: it checks GitHub once a day and asks before installing. Updates are signed and never start or relaunch during a cleaning session. To turn automatic checks off, choose **Keydoze > Settings…**; **Keydoze > Check for Updates…** always works. Anyone on 0.1.0 needs this one download by hand, because that version has no updater.

To build from source instead, you need an Xcode toolchain supporting **Swift 6.2+**:

```sh
git clone https://github.com/dlev02/Keydoze.git
Expand All @@ -41,14 +45,14 @@ swift test --disable-sandbox # core and in-memory event-adapter tests
./script/build_and_run.sh --release # local optimized app; no publication
```

There are no package dependencies. CI passes on macOS 26 with Xcode 26.2, and the native app has been checked on Apple silicon with macOS 27 beta. Interactive macOS 26 and Intel validation are still open. See [development and previews](docs/DEVELOPMENT.md), [release preparation](docs/RELEASING.md), and [contributing](CONTRIBUTING.md).
[Sparkle](https://sparkle-project.org) is the only package dependency. CI passes on macOS 26 with Xcode 26.2, and the native app has been checked on Apple silicon with macOS 27 beta. Interactive macOS 26 and Intel validation are still open. See [development and previews](docs/DEVELOPMENT.md), [release preparation](docs/RELEASING.md), and [contributing](CONTRIBUTING.md).

## Privacy and security

The app processes input only for a user-started session and removes its event tap when the session ends. It saves only your scope and duration preferences. Read the [privacy policy](PRIVACY.md) or [report a security issue](SECURITY.md).
The app processes input only for a user-started session and removes its event tap when the session ends. It saves only your scope, duration, and update preferences. Update checks go to GitHub and can be turned off. Read the [privacy policy](PRIVACY.md) or [report a security issue](SECURITY.md).

## License and credit

Created by **Drew Levinson**. Open source under the [Apache License 2.0](LICENSE).
Created by **Drew Levinson**. Open source under the [Apache License 2.0](LICENSE). Updates use [Sparkle](https://sparkle-project.org), under its [MIT-style license](Resources/Sparkle-LICENSE.txt).

Forks and modifications are welcome. When distributing a derivative, retain the applicable attribution and [NOTICE](NOTICE), include the license, and identify modified files as required by the license. See [contributing](CONTRIBUTING.md#license-and-attribution) for details.
2 changes: 1 addition & 1 deletion Resources/Credits.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
<p>A quiet moment for your Mac.</p>
<p>Created by Drew Levinson.</p>
<p>Open source under the Apache License 2.0.<br>License and attribution are included in this app.</p>
<p>Works offline. No accounts, analytics, or input recordings.</p>
<p>No accounts, analytics, or input recordings. Only update checks use the network.</p>
</body></html>
Binary file added Resources/DMG/dmg-layout.DS_Store
Binary file not shown.
11 changes: 9 additions & 2 deletions Resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,16 @@
<key>CFBundleIconFile</key><string>Keydoze</string>
<key>CFBundleIconName</key><string>Keydoze</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>0.1.0</string>
<key>CFBundleVersion</key><string>3</string>
<key>CFBundleShortVersionString</key><string>0.2.0</string>
<key>CFBundleVersion</key><string>4</string>
<key>LSMinimumSystemVersion</key><string>26.0</string>
<key>NSPrincipalClass</key><string>NSApplication</string>
<key>NSHumanReadableCopyright</key><string>© 2026 Drew Levinson</string>
<key>SUFeedURL</key><string>https://github.com/dlev02/Keydoze/releases/latest/download/appcast.xml</string>
<key>SUPublicEDKey</key><string>M268PTj2i8EyBrX9bPcnoDBSsOR9Juuty+psdt345No=</string>
<key>SUEnableAutomaticChecks</key><true/>
<key>SUAllowsAutomaticUpdates</key><false/>
<key>SURequireSignedFeed</key><true/>
<key>SUVerifyUpdateBeforeExtraction</key><true/>
<key>SUSignedFeedFailureExpirationInterval</key><real>0</real>
</dict></plist>
Loading
Loading