Skip to content
C.W.K.
Lesson 02 of 04 · published

Secure Input Refuses the Whole Command

~12 min · secure-event-input, standing-refusal, passwords, prepare

Level 0Idle Hands
0 XP0/39 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"While secure keyboard entry is active, a hide is refused as a whole and leaves nothing to clean up." — Tinker README

What Secure Keyboard Entry Is

macOS has a system-wide mode called secure event input. An app turns it on while sensitive text is being typed — a password field with focus does it automatically, and Terminal's "Secure Keyboard Entry" menu item turns it on deliberately. While it is on, other processes stop receiving the keystrokes they would otherwise be able to observe. A process can ask whether it is on with the Carbon call IsSecureEventInputEnabled(). Tinker asks.

Why an App-Hider Should Care

Hiding apps never reads a keystroke, so why refuse? The code states the rule rather than its reason, but the reason is not hard to see. Secure input is the clearest signal macOS offers that a person is, right now, typing a secret. Hiding applications at that moment can change which app is in front, and a keystroke meant for a password field that lands somewhere else is exactly the kind of accident an actuator should never be part of. Tinker's architecture puts password and payment interfaces entirely outside its capability; refusing to rearrange the screen while one is in use is the narrowest reading of that rule that the OS lets Tinker check.

A Standing Refusal Belongs in Prepare

The first version checked secure input inside apply, per app — after a receipt had already been written. A refusal there left an uncertain record for an action that had never been attempted, and that record stopped the engine on the next launch. The fix moved the check: HideApplications now asks hideRefusal() in prepare, as soon as it knows at least one app would actually be hidden. A refusal known before the first journal write refuses the whole command and leaves no receipt at all. A preset whose apps are all already hidden is not refused — it had nothing to hide.

The Race Is Covered Too

Secure input can switch on between prepare and the hide of the second app. So setHidden checks again immediately before asking the OS, and throws ChangeNotAttempted — nothing was requested, so the receipt is dropped — and the engine rolls back whatever that command had already hidden. The command fails cleanly either way.

Restore Is Never Refused

Notice that the check applies only to hiding. Showing an app again is not refused under secure input: restore, Quit, logout and emergency release must always be able to put the screen back. A safety rule that could strand a change would trade one risk for another.

Code

The standing refusal, asked in prepare and again before the OS call·swift
import Carbon.HIToolbox

// SystemApplicationVisibility
public func hideRefusal() -> String? {
  IsSecureEventInputEnabled()
    ? "Secure keyboard entry is active; Tinker will not hide apps now."
    : nil
}

// HideApplications.prepare: only when something would actually be hidden
if plans.contains(where: \.needsApply), let refusal = applications.hideRefusal() {
  throw TinkerError(refusal)          // before any receipt exists
}

// SystemApplicationVisibility.setHidden: the race between prepare and apply
if value, let refusal = hideRefusal() { throw ChangeNotAttempted(refusal) }
Seeing the mode from a shell (for the curious)·bash
# Lists the process holding secure input, if any (ioreg output varies by macOS version).
ioreg -l -w 0 | grep -i SecureInput

External links

Exercise

Design the standing refusal for a hypothetical 'mute meeting apps' capability. Which system conditions should refuse the whole command in prepare, which should be checked again just before each effect, and which should never block the undo direction?
Hint
Prepare-time refusals are conditions independent of the individual app: maybe 'a call is being recorded' or 'Do Not Disturb policy forbids changes'. The per-effect re-check covers the same conditions turning on mid-command (throwing ChangeNotAttempted before the OS call). Unmuting — the undo — should never be blocked by any of them, or a safety rule would leave Dad muted.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.