<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Michael Amelin</title>
    <description>The latest articles on DEV Community by Michael Amelin (@michael4kind777).</description>
    <link>https://dev.to/michael4kind777</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4119407%2F84158dea-ec38-46dc-ae6b-d3813bfdce30.png</url>
      <title>DEV Community: Michael Amelin</title>
      <link>https://dev.to/michael4kind777</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/michael4kind777"/>
    <language>en</language>
    <item>
      <title>How Shoo! locks macOS apps behind Touch ID — without a single permission</title>
      <dc:creator>Michael Amelin</dc:creator>
      <pubDate>Wed, 23 Sep 2026 12:16:49 +0000</pubDate>
      <link>https://dev.to/michael4kind777/how-shoo-locks-macos-apps-behind-touch-id-without-a-single-permission-22n</link>
      <guid>https://dev.to/michael4kind777/how-shoo-locks-macos-apps-behind-touch-id-without-a-single-permission-22n</guid>
      <description>&lt;p&gt;A few weeks ago I posted about &lt;a href="https://dev.to/michael4kind777/i-tried-to-break-the-most-popular-mac-app-locker-before-building-my-own-32dg"&gt;breaking the most popular Mac app locker before building my own&lt;/a&gt;. The gist: almost every "app locker" on macOS draws a translucent overlay on top of the app's window and asks Accessibility/Automation permissions to do it. Overlays can be dismissed. Permissions are friction and a privacy smell for a &lt;em&gt;privacy&lt;/em&gt; product.&lt;/p&gt;

&lt;p&gt;This is the promised follow-up: how &lt;a href="https://shooapp.com" rel="noopener noreferrer"&gt;Shoo!&lt;/a&gt; actually works under the hood, and the bugs that taught me why the obvious approaches don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea: don't hide the app, don't let it run
&lt;/h2&gt;

&lt;p&gt;There's no macOS API to "put a password on someone else's app." You can't inject auth into a foreign process. So instead of hiding a running app behind a fake window, Shoo! does this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;NSWorkspace.didLaunchApplicationNotification&lt;/code&gt; / &lt;code&gt;didActivateApplicationNotification&lt;/code&gt; fires for a locked app.&lt;/li&gt;
&lt;li&gt;Shoo! immediately calls &lt;code&gt;NSRunningApplication.forceTerminate()&lt;/code&gt; on it.&lt;/li&gt;
&lt;li&gt;Shoo! brings itself to the front and triggers Touch ID (&lt;code&gt;LAContext.evaluatePolicy&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;On success, it relaunches the app via &lt;code&gt;NSWorkspace.shared.openApplication(at:configuration:)&lt;/code&gt; and marks the bundle ID as unlocked for the session.&lt;/li&gt;
&lt;li&gt;On cancel, the app just stays closed. Nothing to hide, because there's nothing running.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The nice side effect: no full-screen overlay, no Accessibility, no Automation permission. If there's no window to protect, there's no window to click through — which turns out to matter a lot (see the first dead end below).&lt;/p&gt;

&lt;h2&gt;
  
  
  Two dead ends that ate a lot of time
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Overlay windows that intercept clicks.&lt;/strong&gt; Any mouse click that reaches the locker's own window while a Touch ID prompt is pending silently kills the biometric session — no error, no callback, the fingerprint scanner just stops responding until the app restarts. Reproducible with every combination of &lt;code&gt;canBecomeKey&lt;/code&gt;/&lt;code&gt;ignoresMouseEvents&lt;/code&gt; I tried.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Physically hiding the target app instead of killing it.&lt;/strong&gt; Three attempts, three failures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;NSRunningApplication.hide()&lt;/code&gt; silently returns &lt;code&gt;false&lt;/code&gt; for some apps (Telegram, notably).&lt;/li&gt;
&lt;li&gt;AppleScript &lt;code&gt;set visible of process ... to false&lt;/code&gt; "succeeds" and changes nothing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AXUIElementSetAttributeValue(window, kAXMinimizedAttribute, true)&lt;/code&gt; works maybe 60% of the time on the same app.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once I stopped trying to hide a running process and started terminating it instead, an entire category of bugs disappeared.&lt;/p&gt;

&lt;h2&gt;
  
  
  State bookkeeping: the "we already handled this" trap
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;AppMonitorService&lt;/code&gt; tracks two sets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;unlockedForSession&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;   &lt;span class="c1"&gt;// bundle IDs unlocked this session&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;lockingInProgress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;    &lt;span class="c1"&gt;// bundle IDs WE just force-terminated&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;lockingInProgress&lt;/code&gt; exists because &lt;code&gt;didTerminateApplicationNotification&lt;/code&gt; fires for any termination — including the one Shoo! itself just caused. Without ignoring our own termination events, the "unlocked" flag would get reset the instant we set it. The general lesson: a branch that says "we're already handling this, so do nothing" is a good place to hide a bypass. The actual fix has to be "don't re-trigger the check, but still make sure the process is dead" — not "don't touch it at all."&lt;/p&gt;

&lt;h2&gt;
  
  
  The focus-switch cancellation race
&lt;/h2&gt;

&lt;p&gt;If the user doesn't authenticate and switches to a different app instead, the Touch ID prompt has to disappear — not float over someone else's window. That's &lt;code&gt;AuthManager.cancel()&lt;/code&gt; → &lt;code&gt;LAContext.invalidate()&lt;/code&gt;, which maps to &lt;code&gt;LAError.appCancel&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The tricky part: closing the system Touch ID dialog itself briefly hands focus to another process, which looks externally identical to "the user switched apps." Naively watching &lt;code&gt;didActivateApplicationNotification&lt;/code&gt; to cancel the prompt meant: click "Use backup password" → the password sheet appears and instantly disappears.&lt;/p&gt;

&lt;p&gt;Fix: a 1-second desensitization window (&lt;code&gt;ignoreDismissUntil&lt;/code&gt;) after showing the password sheet, plus arming the focus-switch observer only after Shoo! itself becomes the active app (&lt;code&gt;switchAwayArmed&lt;/code&gt;) — otherwise it fires immediately, because closing the locked app hands focus to someone by definition. Activations from the system's own auth agents (&lt;code&gt;com.apple.SecurityAgent&lt;/code&gt;, &lt;code&gt;CoreAuthUI&lt;/code&gt;, &lt;code&gt;coreauthd&lt;/code&gt;) are ignored outright, since they're the ones stealing focus during the dialog.&lt;/p&gt;

&lt;h2&gt;
  
  
  The backup password: PBKDF2, not SHA256
&lt;/h2&gt;

&lt;p&gt;Early version stored the backup password as a bare SHA256 hash. That's close to nothing: an unsalted hash of a short password gets cracked against a rainbow table in seconds if anyone pulls the Keychain entry.&lt;/p&gt;

&lt;p&gt;Current implementation: PBKDF2-SHA256, salted, 210,000 iterations, constant-time comparison, stored as JSON with &lt;code&gt;kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly&lt;/code&gt; (never syncs to iCloud, inaccessible on a locked device). Rate limiting is 5 attempts, then exponential backoff — 5, 10, 20, 40 minutes, capping at an hour. The attempt counter lives in the same Keychain entry, not &lt;code&gt;UserDefaults&lt;/code&gt; — otherwise one &lt;code&gt;defaults delete&lt;/code&gt; resets it to zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five layers to stop someone removing the locker itself
&lt;/h2&gt;

&lt;p&gt;An app locker that can just be deleted doesn't lock anything. This turned out to be the layer where every competitor I tested (see the first post) failed outright.&lt;/p&gt;

&lt;p&gt;The threat model is deliberately narrow: someone who got physical access to an unlocked Mac and doesn't know the admin password. Not "resist a determined attacker with root" — that's not a lock, that's malware.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Quit confirmation.&lt;/strong&gt; Quitting requires Touch ID (or the backup password via an &lt;code&gt;NSAlert&lt;/code&gt;, since SwiftUI sheets inside a status-bar popover are unreliable).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;launchd auto-restart.&lt;/strong&gt; A &lt;code&gt;LaunchAgent&lt;/code&gt; with &lt;code&gt;KeepAlive: true&lt;/code&gt; restarts the process in under a second if it's killed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Startup sweep.&lt;/strong&gt; On launch, any locked app already running gets a soft &lt;code&gt;terminate()&lt;/code&gt; (not &lt;code&gt;forceTerminate()&lt;/code&gt;) — give it a chance to show its own "save changes?" dialog, since biometrics weren't the reason it's closing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bundle watchdog.&lt;/strong&gt; A &lt;code&gt;DispatchSource&lt;/code&gt; watches the app bundle directory for delete/rename events. On tamper: lock down first (clear all "unlocked" state, close protected apps), then try to restore itself from Trash.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root ownership + a root daemon.&lt;/strong&gt; This is the layer that actually mattered.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The real test: App Cleaner &amp;amp; Uninstaller
&lt;/h3&gt;

&lt;p&gt;Layers 1–4 looked solid — until I ran an actual uninstaller against them. App Cleaner &amp;amp; Uninstaller removed the app with zero password prompts, bypassing every layer at once: SIGKILL (uncatchable) → &lt;code&gt;rm -rf&lt;/code&gt; the bundle (no Trash, nothing to restore) → delete the &lt;code&gt;LaunchAgent&lt;/code&gt; plist from &lt;code&gt;~/Library/LaunchAgents&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;None of those three actions require admin rights, because both the app bundle and the LaunchAgent plist live under the user's own home directory.&lt;/p&gt;

&lt;p&gt;Fix: ship as a signed &lt;code&gt;.pkg&lt;/code&gt; that installs the bundle owned by &lt;code&gt;root:wheel&lt;/code&gt; (&lt;code&gt;sudo installer&lt;/code&gt; confirms &lt;code&gt;drwxr-xr-x root wheel&lt;/code&gt;), plus a root &lt;code&gt;launchd&lt;/code&gt; daemon — installed via the &lt;code&gt;.pkg&lt;/code&gt;'s &lt;code&gt;postinstall&lt;/code&gt; script, which already runs with root privileges — that polls every 30 seconds and relaunches the app if it's not running for the logged-in console user. Now an uninstaller has to clear an admin password prompt on both fronts, not zero.&lt;/p&gt;

&lt;p&gt;The correct bypass test (and the one that actually proves the daemon works, not just launchd's normal keep-alive):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;launchctl bootout gui/&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/com.mihailamelin.Shoo.agent
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; ~/Library/LaunchAgents/com.mihailamelin.Shoo.agent.plist
killall &lt;span class="nt"&gt;-9&lt;/span&gt; Shoo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pgrep -x Shoo&lt;/code&gt; confirms the process is gone immediately; ~30 seconds later it's back — this time provably via the daemon, since the launchd job itself was fully unloaded, not just its plist deleted.&lt;/p&gt;

&lt;p&gt;One thing I had to explicitly handle: a legitimate, biometric-confirmed "Quit" shouldn't be treated as an attack. The daemon checks a marker file the app writes on an authenticated quit, and skips relaunching if it's fresh — otherwise "Quit" would look indistinguishable from being killed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The self-update race that only showed up in production
&lt;/h2&gt;

&lt;p&gt;Auto-updates go through Sparkle with &lt;code&gt;installationType="package"&lt;/code&gt; — a separate installer process does the file swap after the user enters their admin password. Two bugs showed up only on a real, already-installed machine, not in development.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 1:&lt;/strong&gt; the bundle watchdog from layer 4 above can't tell a legitimate &lt;code&gt;.pkg&lt;/code&gt; install from an actual attack — both replace files in the bundle the same way. Symptom: click "Install and Relaunch" in the Sparkle dialog, get a "Shoo was removed!" alert instead, which blocks the main run loop and hangs the update. Fix: a flag set at the earliest possible point — the moment the user triggers &lt;code&gt;checkForUpdates()&lt;/code&gt;, before Sparkle touches anything — not in a Sparkle delegate callback, which isn't guaranteed to run before the installer process starts doing file I/O.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 2, more interesting:&lt;/strong&gt; after a successful update, the UI kept showing the old version number until the user manually quit and reopened the app. Two targeted fixes — a marker file for the root daemon, temporarily disabling launchd's &lt;code&gt;KeepAlive&lt;/code&gt; during the update — both failed a live re-test.&lt;/p&gt;

&lt;p&gt;Diagnosis that actually worked: comparing inodes, not paths.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;lsof &lt;span class="nt"&gt;-p&lt;/span&gt; &amp;lt;PID&amp;gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;Contents/MacOS/Shoo   &lt;span class="c"&gt;# inode the running process has open&lt;/span&gt;
&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"%i"&lt;/span&gt; /Applications/Shoo.app/Contents/MacOS/Shoo  &lt;span class="c"&gt;# inode currently on disk&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ps&lt;/code&gt;/&lt;code&gt;pgrep&lt;/code&gt; only show a path string — they can't distinguish the old and new file if the path is identical after a swap. &lt;code&gt;lsof&lt;/code&gt;'s inode for the open executable can. Turned out the running process was sometimes &lt;code&gt;exec()&lt;/code&gt;'d against an already-replaced inode: more than one relauncher (Sparkle, launchd's &lt;code&gt;KeepAlive&lt;/code&gt;, the layer-5 daemon) can race to bring the process back up during a file swap, and "the newest process by start time" isn't always "the process actually running the newest code on disk."&lt;/p&gt;

&lt;p&gt;Instead of chasing every individual relauncher one at a time, the fix stopped caring which one won the race:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// called ~2s after applicationDidFinishLaunching&lt;/span&gt;
&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;selfHealIfLaunchedStale&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;recentlyAttemptedSelfHeal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;inMemoryVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Bundle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;infoDictionary&lt;/span&gt;&lt;span class="p"&gt;?[&lt;/span&gt;&lt;span class="s"&gt;"CFBundleVersion"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as?&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;onDiskVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readVersionDirectlyFromDisk&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// bypasses Bundle's cache&lt;/span&gt;
    &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="n"&gt;inMemoryVersion&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;onDiskVersion&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;writeSelfHealMarker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kt"&gt;NSWorkspace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Bundle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bundleURL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// relaunch the current file on disk&lt;/span&gt;
    &lt;span class="kt"&gt;NSApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;terminate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the code running in memory is older than what's actually on disk right now, relaunch from disk and get out of the way. Confirmed on a real update (1.1.1 → 1.1.2) by checking that the running process's inode matched the on-disk inode exactly — no manual restart needed, version updated itself in the UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's actually novel here, and what isn't
&lt;/h2&gt;

&lt;p&gt;None of the individual pieces are exotic — &lt;code&gt;forceTerminate&lt;/code&gt;, &lt;code&gt;LAContext&lt;/code&gt;, &lt;code&gt;launchctl&lt;/code&gt;, PBKDF2, a shell-script daemon. What's uncommon is applying them together to get a locker with zero TCC permissions, a kill-process model instead of an overlay, and an uninstall-resistance story that survives a real uninstaller instead of just looking good in a demo. Every competitor I profiled for the previous post fails at least one of these three.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://shooapp.com" rel="noopener noreferrer"&gt;Shoo!&lt;/a&gt; is $17.99 once, macOS 14+, not on the App Store (the sandbox forbids terminating other processes — which is the entire mechanism above).&lt;/p&gt;

&lt;p&gt;Happy to go deeper on any piece of this in the comments — the update race in particular took a full evening of live &lt;code&gt;lsof&lt;/code&gt; debugging to actually nail down.&lt;/p&gt;

</description>
      <category>macos</category>
      <category>swift</category>
      <category>security</category>
      <category>indiehackers</category>
    </item>
    <item>
      <title>I tried to break the most popular Mac app locker before building my own</title>
      <dc:creator>Michael Amelin</dc:creator>
      <pubDate>Thu, 10 Sep 2026 13:16:33 +0000</pubDate>
      <link>https://dev.to/michael4kind777/i-tried-to-break-the-most-popular-mac-app-locker-before-building-my-own-32dg</link>
      <guid>https://dev.to/michael4kind777/i-tried-to-break-the-most-popular-mac-app-locker-before-building-my-own-32dg</guid>
      <description>&lt;p&gt;AppLocker sits at the top of the Mac App Store's "app lockers" category with a 4.1★ rating from over 1,000 reviews. On MacUpdate, the same app sits at 2.6★. That gap is what got me curious.&lt;/p&gt;

&lt;p&gt;I'd been building &lt;a href="https://shooapp.com" rel="noopener noreferrer"&gt;Shoo!&lt;/a&gt; — a Touch ID lock for macOS apps — and before shipping it, I wanted to understand what was already out there and why the reviews disagreed so much. So I actually tried to get past AppLocker's lock. Here's what I found.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lock disappears if you delete the app
&lt;/h2&gt;

&lt;p&gt;Locking an app in AppLocker overlays a passcode/Touch ID screen on top of it. But nothing stops you from deleting the locked app from Finder or Launchpad. Do that, reinstall it (or restore from a backup), and the lock is simply gone — no authentication asked, no warning. Whatever was "protected" is now wide open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Editing the app's Bundle ID removes the lock
&lt;/h2&gt;

&lt;p&gt;Every macOS app has an identifier in its Info.plist (its Bundle ID) that tools like AppLocker use to recognize which app to gate. Change that identifier — which just means opening the app package and editing a text file — and the lock no longer recognizes the app as the one it's supposed to protect. The overlay never appears again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The backup password is 4 digits
&lt;/h2&gt;

&lt;p&gt;If you forget your unlock code, AppLocker falls back to a secondary passcode. That fallback is a 4-digit PIN — 10,000 possible combinations, no rate limiting mentioned anywhere in the app or its documentation. That's the kind of number you'd expect from an iPhone in 2008, not from something guarding your Mail or Photos in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  It only checks once — at launch
&lt;/h2&gt;

&lt;p&gt;AppLocker verifies you at the moment an app launches. If the app is already running and you just switch back to it (Cmd+Tab, clicking its Dock icon), there's no second check. So the actual window of protection is much narrower than "this app is locked" implies — it's really "this app asks once, at the start of the session."&lt;/p&gt;

&lt;h2&gt;
  
  
  No password recovery path
&lt;/h2&gt;

&lt;p&gt;If you lock yourself out entirely, there's no account-based recovery, no email reset — you're stuck. For a security tool, having zero recovery path is arguably a bigger liability than the bypasses above, since a lot of the reports on MacUpdate are exactly this: people locked out of their own apps with no way back in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this pattern keeps showing up
&lt;/h2&gt;

&lt;p&gt;None of these are exotic exploits — they're the natural consequence of building a launch-gate, not a real lock. AppLocker draws a screen over the app, or blocks it from opening. It never touches the running process itself. Once the process exists (or the identity check is fooled), the "lock" has nothing left to enforce.&lt;/p&gt;

&lt;p&gt;That's the specific problem I built &lt;a href="https://shooapp.com" rel="noopener noreferrer"&gt;Shoo!&lt;/a&gt; to solve differently: instead of gating the launch, it actually terminates the process when you're not authenticated via Touch ID, and re-checks on every relaunch and app-switch — including Apple's own apps (Messages, Photos, Mail), which most Mac lockers can't touch at all.&lt;/p&gt;

&lt;p&gt;I'm obviously not a neutral party here — I build a competing app, and I want to be upfront about that. But the findings above are reproducible on your own Mac in a few minutes, so I'd rather people just go verify them than take my word for it. Full write-up with more detail: &lt;a href="https://shooapp.com/vs-applocker/?utm_source=devto" rel="noopener noreferrer"&gt;shooapp.com/vs-applocker&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Curious whether other "app locker" style tools for macOS have the same launch-gate-not-a-real-lock problem — if you've poked at one, I'd like to hear what you found.&lt;/p&gt;

</description>
      <category>macos</category>
      <category>indiehackers</category>
      <category>security</category>
    </item>
  </channel>
</rss>
