<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Michael Amelin]]></title><description><![CDATA[Michael Amelin]]></description><link>https://michael4kind.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Michael Amelin</title><link>https://michael4kind.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Thu, 17 Sep 2026 18:04:22 GMT</lastBuildDate><atom:link href="https://michael4kind.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How a Mac App Locker Actually Works (and Why the Obvious Approach Fails)]]></title><description><![CDATA[macOS still has no public API for "lock this one app behind biometrics." You can lock the whole machine. You can put a whole user account behind Touch ID. You cannot say "let everything else run, but ]]></description><link>https://michael4kind.hashnode.dev/how-a-mac-app-locker-actually-works-and-why-the-obvious-approach-fails</link><guid isPermaLink="true">https://michael4kind.hashnode.dev/how-a-mac-app-locker-actually-works-and-why-the-obvious-approach-fails</guid><category><![CDATA[macOS]]></category><category><![CDATA[Security]]></category><category><![CDATA[Swift]]></category><dc:creator><![CDATA[Michael Amelin]]></dc:creator><pubDate>Sun, 13 Sep 2026 21:38:36 GMT</pubDate><content:encoded><![CDATA[<p>macOS still has no public API for "lock this one app behind biometrics." You can lock the whole machine. You can put a whole user account behind Touch ID. You cannot say "let everything else run, but Mail needs my fingerprint first" — Apple's own support staff will tell you, in writing, on their own community forums: <em>"It is not possible to lock apps individually."</em></p>
<p>That gap is why a small category of third-party "app lockers" exists on macOS at all. I spent a while building one (Shoo!), and before landing on an architecture that actually holds up, I went through — and broke — the obvious one first. This is a writeup of both: the approach that looks correct on paper and falls apart in practice, and the one that doesn't.</p>
<h3>The obvious approach: draw a curtain over it</h3>
<p>If there's no API to lock an app, the intuitive workaround is: let the app keep running, and draw something on top of it — a full-screen <code>NSPanel</code> at <code>.screenSaver</code> level, styled to look like a lock screen, sitting between the user and the real window underneath.</p>
<p>It's the approach almost every app locker on the Mac App Store and beyond actually ships with. And it has three failure modes that show up the moment you try to make it airtight, not just demo-able.</p>
<p><strong>1. A stray click can silently kill the biometric prompt.</strong> While a <code>LAContext.evaluatePolicy</code> call is in flight and the Touch ID sheet is up, any mouse click that lands on your own overlay window — not the protected app, your window — can invalidate the sensor without raising an error. No <code>LAError</code>, no callback, nothing. The fingerprint reader just stops responding until the app relaunches. I tried every combination of <code>canBecomeKey</code> and <code>ignoresMouseEvents</code> I could think of; if the click reaches your window at all, it's not reliable.</p>
<p><strong>2. Hiding a running app is not one reliable operation, it's three unreliable ones.</strong></p>
<ul>
<li><code>NSRunningApplication.hide()</code> returns <code>false</code> for some apps for no documented reason (Telegram, in my testing) and does nothing.</li>
<li><code>NSAppleScript</code> with <code>set visible of process "X" to false</code> first throws TCC error <code>-1743</code> until you grant Automation permission and add an <code>NSAppleEventsUsageDescription</code> — and once you clear that hurdle, it reports success while changing nothing.</li>
<li><code>AXUIElementSetAttributeValue(window, kAXMinimizedAttribute, true)</code> works about half the time on the exact same target app, no code changes in between.</li>
</ul>
<p>None of these are "hard but solvable with more code." They're unreliable at the OS level, on apps you don't control, and you can't average three unreliable primitives into one reliable one.</p>
<p><strong>3. Overlay-based locking needs permissions the user should not have to grant.</strong> Automation, sometimes Accessibility, sometimes Screen Recording depending on how the overlay detects what's underneath it. Every one of those is a system prompt that makes a privacy tool look like the thing it's supposed to protect against.</p>
<h3>What actually works: don't hide it, close it</h3>
<p>The alternative that held up under testing is structurally different: instead of letting the protected app keep running and disguising it, you don't let it run at all until authentication succeeds.</p>
<p><code>NSWorkspace</code> publishes two notifications every app locker on macOS can subscribe to without any special entitlement: <code>didLaunchApplicationNotification</code> and <code>didActivateApplicationNotification</code>. The moment a protected bundle ID shows up in either one:</p>
<pre><code class="language-swift">NSRunningApplication
    .runningApplications(withBundleIdentifier: bundleID)
    .first?
    .forceTerminate()

NSApp.activate(ignoringOtherApps: true)
// present LAContext.evaluatePolicy here

// on success:
NSWorkspace.shared.openApplication(
    at: bundleURL,
    configuration: config // .activates = true
)
</code></pre>
<p>That's the entire mechanism. No overlay window exists in the common case — there's nothing on screen to hide, because the protected app's process isn't running. The click-kills-the-sensor bug in the previous approach only existed because there was a window competing for input focus during authentication; here, there's nothing to compete with.</p>
<p>This also removes the permission problem from the previous section entirely, as a side effect rather than a deliberate trade-off. You're not reading another app's window state, so you don't need Accessibility. You're not scripting another app, so you don't need Automation. <code>LocalAuthentication</code> itself needs zero TCC prompts on Mac — it just needs a physical Touch ID sensor or a paired Apple Watch. The security model got simpler because the mechanism got simpler, not the other way around.</p>
<p>The honest trade-off: <code>forceTerminate()</code> is a hard kill. A newly launched app has nothing to lose, but an app that's been running for hours with an unsaved document does. That's a real, unresolved cost of this architecture, and any implementation that claims otherwise for free is glossing over something.</p>
<h3>The harder problem isn't locking, it's re-locking</h3>
<p>Blocking launch is the easy 80%. The question that actually shapes the product is: once someone has authenticated and is using the app, when does it lock again?</p>
<p>The tempting default is an idle timer — X minutes of inactivity, then re-lock. It's what most competing tools ship, and it's also the single most common one-star complaint pattern on them: the timeout fires while someone is mid-sentence in an email, or mid-edit in a document, because "inactive" from the OS's point of view (no keyboard/mouse events) doesn't mean "not using it" from the user's point of view (reading, thinking, on a call).</p>
<p>The alternative that avoids this is to key re-locking off <em>physical absence</em> signals instead of a guessed timeout:</p>
<ul>
<li><code>NSWorkspace.willSleepNotification</code> / <code>screensDidSleepNotification</code> — the lid closed or the display slept.</li>
<li>The distributed notification <code>com.apple.screenIsLocked</code> — screen lock, which <code>NSWorkspace</code> doesn't cover on its own.</li>
</ul>
<p>Critically, none of these should trigger <code>forceTerminate()</code> immediately. If they did, you'd kill a video call or an in-progress download the instant the screen locks. Instead, they should only clear the "already authenticated this session" flag — the app keeps running in the background exactly as it was, and the actual re-authentication check happens lazily, the next time the user brings it back to the foreground.</p>
<p>There's one edge case this creates: on wake, the protected app can already be the frontmost window (that's what was on screen when the Mac went to sleep), and no "activate" event fires because focus never changed. If you only listen for activation, this is a silent hole. The fix is to explicitly re-check <code>NSWorkspace.shared.frontmostApplication</code> on <code>didWakeNotification</code>, with a short delay — the system is still shuffling windows around for a moment after wake, and checking too early reads stale state.</p>
<h3>Storing the fallback password correctly</h3>
<p>Biometrics need a fallback for the no-Touch-ID Mac and the "sensor didn't read it right" case, and that fallback is exactly the kind of thing that's easy to get wrong in a way that doesn't show up until someone tries to break it.</p>
<p>The failure mode worth naming explicitly: a plaintext or bare-SHA256 stored password is one Keychain dump away from being trivially recovered — SHA256 with no salt is exactly what rainbow tables are built for, and a short password has no meaningful entropy against one. The fix is standard but easy to skip under time pressure: PBKDF2-SHA256 with a per-installation random salt and a five-figure iteration count, comparison done in constant time so a timing side-channel can't leak how many leading characters were guessed correctly, and an attempt counter that lives in the same Keychain entry as the hash — not in a plist a <code>defaults delete</code> can zero out — with an escalating lockout (a handful of free attempts, then a cooldown that grows instead of resetting).</p>
<p>None of this is novel cryptography. It's the same PBKDF2-or-better-with-salt advice that applies to literally any password storage. The point is narrower: a tool whose entire pitch is "we protect your private stuff" inherits that obligation for its own fallback secret too, and it's a detail that's invisible right up until it's the whole story.</p>
<h3>Where this leaves things</h3>
<p>The pattern above — terminate on launch/activate, re-check lazily on physical-presence signals rather than a timer, authenticate offline with a properly salted hash — is the architecture behind <a href="https://shooapp.com/?utm_source=hashnode">Shoo!</a>, the Touch ID app locker I built after going through the dead ends above. I've written up a more concrete, name-names comparison of where this diverges from specific existing tools at <a href="https://shooapp.com/vs-applocker/?utm_source=hashnode">shooapp.com/vs-applocker</a> and a broader rundown of why the built-in macOS options (Screen Time, a second user account, FileVault) don't actually solve this problem at <a href="https://shooapp.com/how-to-lock-an-app-on-mac/?utm_source=hashnode">shooapp.com/how-to-lock-an-app-on-mac</a>, if you want the receipts rather than my summary of them.</p>
<p>Disclosure since this is obviously not a neutral source: I'm the author of both this post and Shoo!. The architecture problems described above are real and were real before I had a product attached to the solution — I'd have run into the same overlay dead ends building this as a weekend script for myself.</p>
]]></content:encoded></item></channel></rss>