RepoPilotOpen in app →

aahung/Unshaky

A software attempt to address the "double key press" issue on Apple's butterfly keyboard [not actively maintained]

Mixed

Stale — last commit 2y ago

worst of 4 axes
Use as dependencyMixed

last commit was 2y ago; no tests detected

Fork & modifyHealthy

Has a license, tests, and CI — clean foundation to fork and modify.

Learn fromHealthy

Documented and popular — useful reference codebase to read through.

Deploy as-isHealthy

No critical CVEs, sane security posture — runnable as-is.

  • 13 active contributors
  • MIT licensed
  • CI configured
Show 3 more →
  • Stale — last commit 2y ago
  • Concentrated ownership — top contributor handles 75% of recent commits
  • No test directory detected
What would change the summary?
  • Use as dependency MixedHealthy if: 1 commit in the last 365 days

Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests

Informational only. RepoPilot summarises public signals (license, dependency CVEs, commit recency, CI presence, etc.) at the time of analysis. Signals can be incomplete or stale. Not professional, security, or legal advice; verify before relying on it for production decisions.

Embed the "Forkable" badge

Paste into your README — live-updates from the latest cached analysis.

Variant:
RepoPilot: Forkable
[![RepoPilot: Forkable](https://repopilot.app/api/badge/aahung/unshaky?axis=fork)](https://repopilot.app/r/aahung/unshaky)

Paste at the top of your README.md — renders inline like a shields.io badge.

Preview social card (1200×630)

This card auto-renders when someone shares https://repopilot.app/r/aahung/unshaky on X, Slack, or LinkedIn.

Onboarding doc

Onboarding: aahung/Unshaky

Generated by RepoPilot · 2026-05-10 · Source

🤖Agent protocol

If you are an AI coding agent (Claude Code, Cursor, Aider, Cline, etc.) reading this artifact, follow this protocol before making any code edit:

  1. Verify the contract. Run the bash script in Verify before trusting below. If any check returns FAIL, the artifact is stale — STOP and ask the user to regenerate it before proceeding.
  2. Treat the AI · unverified sections as hypotheses, not facts. Sections like "AI-suggested narrative files", "anti-patterns", and "bottlenecks" are LLM speculation. Verify against real source before acting on them.
  3. Cite source on changes. When proposing an edit, cite the specific path:line-range. RepoPilot's live UI at https://repopilot.app/r/aahung/Unshaky shows verifiable citations alongside every claim.

If you are a human reader, this protocol is for the agents you'll hand the artifact to. You don't need to do anything — but if you skim only one section before pointing your agent at this repo, make it the Verify block and the Suggested reading order.

🎯Verdict

WAIT — Stale — last commit 2y ago

  • 13 active contributors
  • MIT licensed
  • CI configured
  • ⚠ Stale — last commit 2y ago
  • ⚠ Concentrated ownership — top contributor handles 75% of recent commits
  • ⚠ No test directory detected

<sub>Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests</sub>

Verify before trusting

This artifact was generated by RepoPilot at a point in time. Before an agent acts on it, the checks below confirm that the live aahung/Unshaky repo on your machine still matches what RepoPilot saw. If any fail, the artifact is stale — regenerate it at repopilot.app/r/aahung/Unshaky.

What it runs against: a local clone of aahung/Unshaky — the script inspects git remote, the LICENSE file, file paths in the working tree, and git log. Read-only; no mutations.

| # | What we check | Why it matters | |---|---|---| | 1 | You're in aahung/Unshaky | Confirms the artifact applies here, not a fork | | 2 | License is still MIT | Catches relicense before you depend on it | | 3 | Default branch master exists | Catches branch renames | | 4 | 5 critical file paths still exist | Catches refactors that moved load-bearing code | | 5 | Last commit ≤ 932 days ago | Catches sudden abandonment since generation |

<details> <summary><b>Run all checks</b> — paste this script from inside your clone of <code>aahung/Unshaky</code></summary>
#!/usr/bin/env bash
# RepoPilot artifact verification.
#
# WHAT IT RUNS AGAINST: a local clone of aahung/Unshaky. If you don't
# have one yet, run these first:
#
#   git clone https://github.com/aahung/Unshaky.git
#   cd Unshaky
#
# Then paste this script. Every check is read-only — no mutations.

set +e
fail=0
ok()   { echo "ok:   $1"; }
miss() { echo "FAIL: $1"; fail=$((fail+1)); }

# Precondition: we must be inside a git working tree.
if ! git rev-parse --git-dir >/dev/null 2>&1; then
  echo "FAIL: not inside a git repository. cd into your clone of aahung/Unshaky and re-run."
  exit 2
fi

# 1. Repo identity
git remote get-url origin 2>/dev/null | grep -qE "aahung/Unshaky(\\.git)?\\b" \\
  && ok "origin remote is aahung/Unshaky" \\
  || miss "origin remote is not aahung/Unshaky (artifact may be from a fork)"

# 2. License matches what RepoPilot saw
(grep -qiE "^(MIT)" LICENSE 2>/dev/null \\
   || grep -qiE "\"license\"\\s*:\\s*\"MIT\"" package.json 2>/dev/null) \\
  && ok "license is MIT" \\
  || miss "license drift — was MIT at generation time"

# 3. Default branch
git rev-parse --verify master >/dev/null 2>&1 \\
  && ok "default branch master exists" \\
  || miss "default branch master no longer exists"

# 4. Critical files exist
test -f "Unshaky/KeyEventHandler.swift" \\
  && ok "Unshaky/KeyEventHandler.swift" \\
  || miss "missing critical file: Unshaky/KeyEventHandler.swift"
test -f "Unshaky/AppDelegate.swift" \\
  && ok "Unshaky/AppDelegate.swift" \\
  || miss "missing critical file: Unshaky/AppDelegate.swift"
test -f "Podfile" \\
  && ok "Podfile" \\
  || miss "missing critical file: Podfile"
test -f ".travis.yml" \\
  && ok ".travis.yml" \\
  || miss "missing critical file: .travis.yml"
test -f "Unshaky/PreferencesWindowController.swift" \\
  && ok "Unshaky/PreferencesWindowController.swift" \\
  || miss "missing critical file: Unshaky/PreferencesWindowController.swift"

# 5. Repo recency
days_since_last=$(( ( $(date +%s) - $(git log -1 --format=%at 2>/dev/null || echo 0) ) / 86400 ))
if [ "$days_since_last" -le 932 ]; then
  ok "last commit was $days_since_last days ago (artifact saw ~902d)"
else
  miss "last commit was $days_since_last days ago — artifact may be stale"
fi

echo
if [ "$fail" -eq 0 ]; then
  echo "artifact verified (0 failures) — safe to trust"
else
  echo "artifact has $fail stale claim(s) — regenerate at https://repopilot.app/r/aahung/Unshaky"
  exit 1
fi

Each check prints ok: or FAIL:. The script exits non-zero if anything failed, so it composes cleanly into agent loops (./verify.sh || regenerate-and-retry).

</details>

TL;DR

Unshaky is a macOS menu bar utility written in Swift and Objective-C that suppresses unwanted double key presses on Apple's butterfly keyboard by filtering out duplicate keystrokes occurring within a configurable time window (default milliseconds). It hooks into macOS keyboard events at the system level to detect and discard repeated presses of the same key, solving a notorious hardware defect affecting MacBook Pro 2016+ and MacBook Air 2018+ models. Single-target macOS application: the project is a monolithic Swift/Objective-C Xcode app (Podfile present indicates CocoaPods dependency management) with a menu bar interface (References to 'menubar.png' in README). Core logic is likely in the main application target with keyboard event interception via Objective-C system APIs; test infrastructure uses Nimble (Pods/Nimble) for test matchers.

👥Who it's for

MacBook owners with butterfly keyboard hardware defects (primarily 2016–2019 models) who experience involuntary double key presses and cannot immediately access Apple's keyboard service program. Users range from writers and developers to general office workers seeking a software workaround before keyboard replacement.

🌱Maturity & risk

Maintenance-mode project: the repo shows active CI via Travis CI (.travis.yml present) and test infrastructure via Nimble (Pods/Nimble), but the maintainer explicitly states 'Unshaky is in maintenance mode, and I will only work on bug fixes.' The README flags a critical issue on macOS 10.15.6+ requiring workaround (issue #166), indicating the project has stability concerns on recent OS versions despite historical GitHub release activity.

High risk for long-term use: (1) project is in maintenance-only mode with no active feature development, (2) known breakage on macOS 10.15.6+ without guaranteed fix, (3) single maintainer (aahung) with no visible succession plan, (4) butterfly keyboard hardware is now largely obsolete (Apple discontinued it by 2020), making the user base shrinking. New macOS versions may break compatibility without patches.

Active areas of work

No active development: the repo is in maintenance mode per the README notice. Last visible activity appears to be bug fixes and CI configuration (.travis.yml). The most recent flagged issue is macOS 10.15.6 incompatibility (issue #166), indicating recent users are hitting regressions but no active resolution is underway.

🚀Get running

git clone https://github.com/aahung/Unshaky.git
cd Unshaky
pod install
open Unshaky.xcworkspace

Then build and run the Unshaky target in Xcode. The Podfile defines CocoaPods dependencies; pod install generates the .xcworkspace file required to build.

Daily commands: After pod install and opening Unshaky.xcworkspace, build the Unshaky scheme in Xcode and run it. The app installs a menu bar icon; users configure the key repeat threshold in the UI. No separate dev server—it's a native macOS application.

🗺️Map of the codebase

  • Unshaky/KeyEventHandler.swift — Core event handler that intercepts keyboard events and implements the double-key-press suppression logic—the heart of the application
  • Unshaky/AppDelegate.swift — Application entry point and lifecycle manager; initializes keyboard event monitoring and system permissions
  • Podfile — Dependency manifest defining testing frameworks (Nimble, Quick) and build configuration—required for reproducible builds
  • .travis.yml — CI/CD pipeline configuration; documents build and test process for contributors
  • Unshaky/PreferencesWindowController.swift — UI controller for user settings including debounce threshold configuration—directly affects double-key filtering behavior
  • Unshaky/StatusMenuController.swift — Menu bar UI and application state management; user-facing interface for enabling/disabling the fix
  • Unshaky/KeyEvent.swift — Data model representing keyboard events; core abstraction for event filtering logic

🧩Components & responsibilities

  • KeyEventHandler (Swift, dictionary for event state, timestamp comparison) — Core filtering logic: maintains timestamp history, compares incoming events against recent presses, suppresses duplicates within threshold
    • Failure mode: If threshold is too low, legitimate key repeats suppressed; if too high, doubles still appear
  • AppDelegate (AppKit, Quartz Event Tap API, NotificationCenter) — Lifecycle orchestrator: requests permissions, installs event tap, manages app launch/termination
    • Failure mode: If permissions denied or tap installation fails, keyboard monitoring silently disabled
  • PreferencesWindowController (AppKit, NSWindowController, NSSlider, UserDefaults binding) — User-facing settings UI for threshold adjustment and advanced configuration
    • Failure mode: Invalid threshold values could break deduplication; no range validation shown
  • UserPreferences (Swift, UserDefaults, property wrappers) — Abstraction layer for persisting and retrieving user settings from UserDefaults
    • Failure mode: Corrupted defaults file could cause invalid threshold or crash on startup
  • StatusMenuController (AppKit, NSStatusBar, NSMenu) — Menu bar UI: displays app state, toggle enable/disable, access preferences
    • Failure mode: If menu is unresponsive, user cannot control app from menu bar

🔀Data flow

  • macOS OSQuartz Event Tap (in KeyEventHandler) — User presses a key on the butterfly keyboard
  • KeyEventHandlerKeyEvent model — Raw system event converted to structured KeyEvent with code, timestamp, flags
  • KeyEventKeyEventHandler deduplication logic — Event compared against recent history; timestamp checked against debounce threshold
  • KeyEventHandlerUserPreferences — Threshold value loaded from UserDefaults on app start; reloaded on preference change
  • KeyEventHandlermacOS OS — Filtered

🛠️How to make changes

Add a new debounce threshold setting

  1. Define the new preference key and default value in UserPreferences.swift (Unshaky/UserPreferences.swift)
  2. Add UI control (slider/input field) in PreferencesWindowController.swift to expose the setting (Unshaky/PreferencesWindowController.swift)
  3. Update KeyEventHandler.swift to read and apply the new threshold during event filtering (Unshaky/KeyEventHandler.swift)
  4. Write tests in UnShakyTests/KeyEventHandlerTests.swift to verify threshold behavior (UnShakyTests/KeyEventHandlerTests.swift)

Add support for a new key or modifier combination

  1. Extend the Key enum in Key.swift with the new key code and mapping (Unshaky/Key.swift)
  2. Update KeyEvent.swift if special handling is needed for the new key or modifier flags (Unshaky/KeyEvent.swift)
  3. Add comparison logic in KeyEventHandler.swift if the new key requires custom deduplication rules (Unshaky/KeyEventHandler.swift)
  4. Add test cases in UnShakyTests/KeyEventHandlerTests.swift to verify correct behavior (UnShakyTests/KeyEventHandlerTests.swift)

Add a new UI status indicator or menu item

  1. Create or modify the status menu view in StatusMenuController.swift (Unshaky/StatusMenuController.swift)
  2. Update AppDelegate.swift to broadcast relevant state changes (e.g., via NotificationCenter) (Unshaky/AppDelegate.swift)
  3. Bind the new UI element to state updates in StatusMenuController.swift or a new view controller (Unshaky/StatusMenuController.swift)

🔧Why these technologies

  • macOS Quartz Event Tap (CGEventTap) — Provides low-level global keyboard event interception before events reach applications, enabling filtering of duplicate key presses
  • Swift + Cocoa (AppKit) — Native macOS UI framework required for system integration, menu bar support, and accessibility API access
  • UserDefaults — Lightweight, built-in persistence for user preferences without external database dependency
  • CocoaPods + Nimble + Quick — Provides mature testing framework for validating event filtering logic and edge cases in CI/CD

⚖️Trade-offs already made

  • Global keyboard event tap via Quartz instead of per-application monitoring

    • Why: Simpler implementation; applies fix uniformly across all apps without per-app configuration
    • Consequence: Requires accessibility permissions (more intrusive); slight global performance overhead; cannot be application-specific
  • Simple timestamp-based deduplication instead of hardware-level firmware fix

    • Why: Software-only solution; no hardware modification; works across different MacBook models
    • Consequence: May not catch pathological cases where hardware sends events >threshold apart; timing-dependent behavior
  • Maintenance mode with bug-fix-only approach

    • Why: Limited resources and macOS compatibility drift (noted issues with 10.15.6+)
    • Consequence: New features unlikely; long-term sustainability at risk; users may need OS workarounds

🚫Non-goals (don't propose these)

  • Does not fix hardware-level keyboard manufacturing defects—only masks symptoms in software
  • Does not support customization per application or keyboard type
  • Does not provide real-time event statistics or detailed logging by default
  • Does not support Linux or Windows platforms

🪤Traps & gotchas

macOS 10.15.6+ breakage: The app is non-functional on macOS Catalina (10.15.6) and later without undocumented workarounds (see issue #166); any contributor testing on modern macOS will hit this immediately. System permissions: The app requires keyboard event interception permissions (Input Monitoring in System Preferences) which may silently fail if not granted—no obvious error handling visible. Objective-C/Swift bridge: Keyboard event hooks are likely in Objective-C using deprecated or fragile IOKit APIs; touching this code risks breaking the entire app. No visible main source: The actual keyboard filtering logic is not in the file list provided (likely in Xcode project internals not shown), making it difficult to locate without opening the .xcodeproj.

🏗️Architecture

💡Concepts to learn

  • IOKit Event Tap — Unshaky must intercept keyboard events at the system level before they reach applications; Event Taps are the primary macOS API for this, and understanding their limitations (passive vs. active taps, permissions, event ordering) is essential to debugging the core functionality
  • Debouncing and Key Repeat Suppression — The core algorithm must distinguish between legitimate rapid key repeats (user holding a key) and unintended hardware-induced doubles; this is a classic debouncing problem with unique hardware-specific constraints
  • Menu Bar Applications (NSStatusBar) — Unshaky lives in the macOS menu bar; NSStatusBar/NSStatusItem are the framework APIs for background apps with menu bar icons, and understanding their lifecycle and permissions model is necessary for UI modifications
  • CocoaPods Dependency Management — The project uses Podfile and pod install (not Swift Package Manager); understanding CocoaPods version constraints, transitive dependencies, and the .xcworkspace workflow is required for dependency updates
  • Objective-C/Swift Interoperability — The codebase mixes Swift (37KB) and Objective-C (25KB); low-level keyboard APIs are in Objective-C while UI is in Swift; mastering bridging headers and memory management across languages is essential for modifications
  • macOS Keyboard Event Filtering and Permissions (Input Monitoring) — macOS Catalina+ requires explicit user permission for apps to monitor keyboard input (Input Monitoring in System Preferences); Unshaky's breakage on 10.15.6+ is partially due to stricter permission enforcement, and understanding this security model prevents regression
  • BDD Testing with Nimble — The test infrastructure uses Nimble (Pods/Nimble), a Swift BDD matcher framework; reading DSL.swift and other Nimble sources is necessary to write assertions in the project's testing idiom rather than raw XCTest
  • keyboardlayoutguide/KeyboardLayoutGuide — Another macOS keyboard-focused utility; demonstrates modern Swift/Objective-C interop patterns for system-level keyboard APIs
  • Homebrew/homebrew-cask — Unshaky is distributed via Homebrew Cask; understanding cask definitions helps package maintenance and distribution
  • macmade/KeyboardAndMouse — Similar macOS menu bar utility for keyboard customization; shows alternative UI patterns for settings panels
  • apple/darwin-xnu — Darwin kernel source; critical reference for understanding IOKit Event Tap and Objective-C keyboard interception APIs that Unshaky likely uses
  • nim-lang/nimble — The Nimble testing framework used in Pods/Nimble; understanding its assertion DSL directly improves test writing in this project

🪄PR ideas

To work on one of these in Claude Code or Cursor, paste: Implement the "<title>" PR idea from CLAUDE.md, working through the checklist as the task list.

Add GitHub Actions CI workflow to replace Travis CI

The repo uses outdated .travis.yml for CI/CD. Travis CI has changed its pricing model and this repo would benefit from migrating to GitHub Actions, which is free for public repos. This is critical since the project is in maintenance mode and needs reliable automated testing for any contributions.

  • [ ] Create .github/workflows/build-and-test.yml with macOS build matrix
  • [ ] Configure workflow to run Swift tests using xcodebuild with Nimble test framework (already in Podfile)
  • [ ] Add workflow to validate code signing and notarization steps if applicable
  • [ ] Remove or archive .travis.yml after migration is verified

Add unit tests for keyboard event filtering logic

The repo handles double-key-press detection which is complex state management. There's a Podfile with Nimble testing framework already configured but no visible test files in the file structure. Unit tests for the core debouncing/filtering logic would prevent regressions and help future maintainers understand the keyboard event handling.

  • [ ] Create Tests/ directory structure for XCTest + Nimble integration
  • [ ] Identify and test the keyboard event filtering mechanism (likely in main source, not visible in provided structure)
  • [ ] Add tests for edge cases: rapid key sequences, different key combinations, timing boundaries
  • [ ] Ensure pod install includes test dependencies and configure Podfile for test targets

Document macOS 10.15.6+ compatibility workaround in README

The README mentions a critical issue: 'on macOS 10.15.6, Unshaky no longer works unless by default' with a reference to issue #166, but provides no actual workaround steps or explanation. This leaves users confused. Adding a troubleshooting section would reduce support burden on maintainers.

  • [ ] Review issue #166 to understand the root cause and workaround
  • [ ] Create a 'Troubleshooting' section in README.md with macOS version-specific guidance
  • [ ] Document the default behavior settings that resolve the 10.15.6 issue
  • [ ] Add a compatibility matrix showing tested macOS versions and known limitations

🌿Good first issues

  • Add unit tests for the key repeat filtering logic using Nimble matchers (e.g., verify that a key press 5ms after the previous one is suppressed but 100ms later is allowed)—currently no visible test coverage in the file list for the core deduplication algorithm.
  • Document the macOS 10.15.6+ workaround in a separate TROUBLESHOOTING.md file with step-by-step screenshots, since the README only mentions issue #166 without explaining the fix—improves onboarding for new users hitting this blocker.
  • Create a GitHub Actions workflow (replace or supplement .travis.yml) that tests against multiple macOS versions (10.14, 10.15, 11+) to catch OS compatibility regressions early, since the project is in maintenance mode and needs automated regression detection.

Top contributors

Click to expand

📝Recent commits

Click to expand
  • 057fb54 — Update README.md about project status (aahung)
  • aeffed4 — Add some spanish translations (#191) (Lloople)
  • 181ba1c — Update Community Contributors list (xpctnc)
  • b9c76fd — Update Polish translation for v0.7.0 (xpctnc)
  • feac009 — change from "предположительно" на "оптимально" (poteminr)
  • 71d3cd7 — Turkish translation update + username correction #174 from hazarek/master (aahung)
  • 71c71a2 — Update Italian translation for v0.7.0 #175 (aahung)
  • 76f02e0 — Update Italian contributors (danieletorelli)
  • fd15c16 — Update Italian translation for v0.7.0 (danieletorelli)
  • ab11320 — Turkish translation update + typo (hazar)

🔒Security observations

Unshaky presents a moderate security posture with several concerns. The primary risk is that the project is in maintenance mode with only bug fixes, meaning new security vulnerabilities may not be addressed proactively. The application requires elevated system privileges (Accessibility permissions) to function, which increases the potential impact of any security compromise. Dependency management lacks visibility into automated scanning and update processes. The project lacks formal security documentation and disclosure policies. Recommended actions: establish an automated dependency audit process, implement SAST in CI/CD, create formal security policies, and evaluate the long-term viability of the project given its maintenance-only status.

  • High · Unmaintained Project with Known Compatibility Issues — README.md, Project Status. The project is in maintenance mode with only bug fixes being addressed. There is a documented issue (issue #166) indicating the application does not work on macOS 10.15.6 and later without workarounds. This suggests the codebase may not be receiving security updates for newer OS versions. Fix: Either actively maintain the project with regular security and compatibility updates, or clearly document the EOL status and recommend users discontinue use. Implement automated security testing in CI/CD pipeline.
  • Medium · Potential Outdated Dependencies — Podfile, Podfile.lock, Pods/. The project uses CocoaPods for dependency management (Podfile present). The Pods directory contains Nimble testing framework with nested Carthage checkouts. There is no evidence of dependency version pinning strategy or regular dependency audits. Outdated dependencies may contain known security vulnerabilities. Fix: Implement automated dependency scanning using tools like CocoaPods security auditing. Pin dependency versions in Podfile.lock and regularly update dependencies. Add CI/CD checks for vulnerable dependencies using tools like OWASP Dependency-Check or CocoaPods Specs auditing.
  • Medium · Keyboard Event Interception Requires Elevated Privileges — Project purpose (keyboard event handling). Unshaky intercepts keyboard events to address double key press issues. This requires Accessibility permissions on macOS. While this is legitimate functionality, the application has elevated system access that could be abused if the application itself is compromised. Fix: Implement strict code signing and notarization requirements. Document the required permissions clearly. Use macOS security frameworks (Secure Coding Guide) to minimize attack surface. Implement input validation and sanitization for all intercepted keyboard events.
  • Low · No Evidence of Security Headers or Configuration — Repository root. No security configuration files detected (security.txt, SECURITY.md policy file). There is no documented security policy, vulnerability disclosure process, or security guidelines for contributors. Fix: Create a SECURITY.md file documenting: vulnerability disclosure policy, security contact information, supported versions, and patch timeline. Add security.txt file following RFC 9110 standards.
  • Low · Insufficient Build Configuration Audit Trail — .travis.yml. The .travis.yml file is present but contents are not provided. Without visibility into CI/CD configuration, it's unclear if builds include security scanning, code signing verification, or security testing. Fix: Implement security scanning in CI/CD pipeline including: SAST (Static Application Security Testing), dependency scanning, and code signing verification. Document the build security process.
  • Low · Test Framework Dependencies May Have Security Gaps — Pods/Nimble/. The Nimble testing framework and nested CwlPreconditionTesting/CwlCatchException dependencies are present. While test dependencies are typically lower risk, outdated test framework versions could still contain vulnerabilities. Fix: Regularly audit and update test framework dependencies. Review the licenses and security posture of nested dependencies (CwlPreconditionTesting, CwlCatchException).

LLM-derived; treat as a starting point, not a security audit.


Generated by RepoPilot. Verdict based on maintenance signals — see the live page for receipts. Re-run on a new commit to refresh.

Mixed signals · aahung/Unshaky — RepoPilot