# Keyboard approval shortcuts that resist misfires

A keyboard approval control is part of the authorization boundary, not a convenience feature with a security label. If a routine terminal keystroke can pass through a focus change, a freshly opened modal, or a queue of repeated input and authorize an agent call, the control did not collect a decision. It collected timing.

That failure is easy to ship because the happy path feels excellent. A developer triggers an action, an approval window appears, Return accepts it, and everyone calls the interaction fast. Then the same developer is reading terminal output, holds a modifier while switching windows, or presses Return to dismiss an unrelated prompt. The approval arrives at the wrong moment and the action gets consent that nobody consciously gave.

## An approval shortcut is a security boundary

An approval shortcut must represent a new, attributable decision about one live action. The fact that the user owns the keyboard, or that the requesting process is allowed to ask, does not turn every character into approval.

There are three separate facts that teams routinely collapse into one:

- A process may request an action.
- A human may be present at the machine.
- That human may approve this exact action now.

The first fact is about process identity. The second is about local presence. The third is authorization. A signed process can establish the first fact well. A visible window may suggest the second. Neither proves the third.

Treat the approval UI as a small state machine with a narrow entrance to the approved state. It should know which request it shows, whether it currently owns input, when it became eligible for keyboard activation, and whether the request is still valid. When any of those facts changes, invalidate the old route to approval.

A familiar implementation error puts the shortcut on a broad command handler and checks only whether an approval window exists. That looks harmless in a demo. In use, it means an event delivered late can approve a different request, an old request after replacement, or a window that has just returned from the background. The button did not need to be visible at the instant the person pressed the key.

The fix is not to remove all shortcuts. Mouse-only confirmation is hostile to people who use the keyboard well, and it pushes them toward hurried pointer clicks. The fix is to make a keyboard activation harder to qualify than a button label. The system should accept it only when the current dialog, current request, current focus generation, and current physical press agree.

This distinction changes code review. Ask "what exact event can cross this boundary?" rather than "does Return click the button?" A good answer names the event source, the window state, the request identifier, and the rejection cases. "The dialog is open" is not an answer.

## Focus must be proved, not presumed

A dialog should discard keyboard eligibility whenever it loses focus, even if it becomes focused again a moment later. Focus transitions are where input intended for one application surface gets interpreted by another.

Consider a common sequence. An agent is running a command in a terminal. The user types a command, switches to documentation, then a modal authorization window appears. The operating system changes the active window while the user's hands are still moving. Depending on event ordering and the app framework, a Return press can arrive after the modal takes focus even though the user formed the intent before they saw it.

The unsafe rule is simple: if the authorization button is the default button and the window is frontmost, accept Return. That rule treats frontmost status as proof that the person saw and evaluated the request. It proves only that the window occupied the front of the stack at dispatch time.

Use a focus generation instead. When the dialog becomes active, increment a counter and mark keyboard activation as disarmed. Start accepting a qualifying press only after the app has observed focus settled for the current generation and after it sees a new press from the user. On deactivation, clear the armed state immediately. Do not restore it automatically when focus returns.

This is deliberately conservative. A user who switches away and back must press the shortcut once after returning. That extra press is cheap. Accidentally sending a credentialed HTTP call or an SSH command is not.

The same rule applies when the system displays another modal sheet, a password prompt, an accessibility overlay, a notification interaction, or a workspace switch. Your app cannot know why focus moved, and it should not guess. It can know that its earlier assumptions no longer hold.

Do not rely solely on visual focus rings. They tell the user where input appears to go, but an approval decision needs a state transition that tests the app's actual active window and the dialog's current generation. The visual state and the authorization state should change together from one source of truth.

A useful manual test is to open an approval dialog, hold a modifier, switch away, return, and press Return immediately. Repeat it with a pointer click outside the dialog, a system alert, and a fast application switch. Every attempt should leave the request pending until a new explicit action occurs. If even one variation approves, the keyboard path is too broad.

## Held input needs a separate state machine

A key that was already down before the dialog became eligible cannot authorize the dialog. This rule covers the class of bugs that appears when a person holds Return, Space, Escape, or a modifier while the UI changes underneath them.

Event APIs often provide down, repeat, and up phases, but application code tends to reduce them to "received Return." That reduction throws away the information that matters. A press that started in a terminal has a different meaning from a press that starts after the approval request is visible and focused.

Track the physical lifecycle for each accepted shortcut. The dialog can accept a shortcut only when it observes a non-repeat down event after its own activation epoch, while it has focus, followed by whatever completion rule your platform uses. If the event is already down when the dialog opens, set a blocker for that key and wait for its up event. Do not infer a fresh press from a repeat.

Space deserves the same care as Return. Many controls use Space for keyboard activation, and people hold Space for previews, scrolling, or accessibility actions. Escape also needs scrutiny. It should cancel only the live request on screen, and it must not cancel a replacement request that appeared after the user pressed it.

Modifier combinations need a stricter policy. Avoid treating bare modifier state as permission, and avoid shortcuts that overlap with terminal habits such as Control-C, Control-D, or Control-R. If you choose a chord, require the complete chord to begin after the dialog arms, and reject it if focus changed while any member was down. A chord that begins in another window is not a decision about your dialog.

A small internal record is enough:

```
requestId: 84f2
focusGeneration: 17
armedAfterEvent: 912
returnIsBlockedUntilUp: true
approvalState: pending
```

The names do not matter. The rule does: an event must be newer than the dialog's current focus generation and belong to a fresh input lifecycle. Teams often add this state after a report of an accidental approval. Put it in before you add the shortcut.

## Modal timing turns normal input into authorization

A modal that appears at the wrong instant can receive input that the person meant for the underlying task. The danger comes from event timing, not from whether the dialog has a polished confirmation message.

There are several timing gaps worth naming. A request can arrive between a key down and key up. The UI can render before application state finishes attaching the request identifier. A previous request can close while a queued callback still holds its completion handler. A new dialog can reuse the same button object, leaving an old keyboard action pointed at new content.

The familiar failure looks like this:

1. The user presses Return in a terminal to submit a command.
2. The agent process asks for permission while the press is in flight.
3. The app presents an approval dialog and marks its default button active.
4. The key up or a repeated event reaches the new responder.
5. The app treats that event as approval and sends the action.

The user may never read the dialog. The application still writes an audit entry that says the user approved, which makes the failure worse. The record is accurate about the code path and false about the human act.

Prevent this with an activation barrier. On presentation, record a monotonic input sequence or timestamp from the event subsystem and refuse events that began before that barrier. If the platform cannot give you a reliable sequence, require a complete fresh press after the dialog is visibly active. Prefer the conservative design when the platform leaves ambiguity.

Use request generations as well as input generations. Bind each button action and keyboard handler to an immutable request token. When the dialog closes, revoke that token. When another request replaces it, create a new token rather than mutating the old object. A callback that carries an old token should return a denial, even if a new dialog happens to be visible.

Do not try to solve timing solely with a delay such as "ignore Return for 300 milliseconds." Delays are popular because they are easy to explain and easy to code. They fail for slow machines, rapid focus changes, accessibility input, and people who legitimately take longer. A delay measures time. You need to measure a relationship between the input event and the current dialog state.

## Terminal muscle memory is hostile input

Terminal work produces exactly the keystrokes an approval dialog must distrust. Developers press Return to submit commands, Control-C to stop work, Space to page through output, and Escape to abandon an edit. Agents make terminal activity more frequent, so the authorization window appears beside a stream of habitual input.

Do not assume that a terminal sits behind a separate application boundary. People use embedded terminals, split panes, remote shells, full screen windows, and tool panes that look like terminals. A process may also trigger an authorization request just after output tells the developer to press a key. The visual story can change faster than the user's motor plan.

The safe default is to make an approval dialog consume no inherited input. It may receive a fresh Return press after focus and arming, but it must reject the Return that submitted a shell command, the Space held for paging, and any repeat generated by either key. If the UI cannot establish that distinction, remove the shortcut and require a pointer or biometric confirmation.

Do not bind dangerous approval to terminal-shaped commands in a global command map. A global listener sees events beyond the modal's local responder chain and makes it difficult to prove where an activation began. Keep the handler attached to the specific live dialog, then require the dialog to verify its own request token before it asks the authorization layer to act.

This is also where copy and paste deserves attention. A pasted newline can be ordinary text in a terminal and an activation in a form control. Approval controls should ignore text insertion and command dispatch that lacks a qualifying physical shortcut event. A pasted character is data, not consent.

Test with a real terminal workflow, not only a synthetic window. Start a command that prompts repeatedly, trigger a request while pressing Return, and test after using Control-C and Space. Test when the terminal has focus, when a documentation window has focus, and when the authorization window appears during a window switch. The point is to reproduce intent that began elsewhere.

## Test the event history, not the painted button

The most useful tests feed a reducer a hostile event history and assert that it cannot emit approval until the exact right sequence occurs. Snapshot tests can confirm that a dialog looks focused. They cannot tell you whether a stale event crossed the boundary.

Model the dialog with explicit events such as `present`, `focusGained`, `focusLost`, `keyDown`, `keyRepeat`, `keyUp`, `requestRevoked`, and `approveClick`. Give every event a request token and a monotonically increasing input sequence. The reducer should produce one of three outcomes: remain pending, cancel, or emit approval for a matching token.

This fixture is deliberately small enough to keep beside the interaction code:

```json
[
  {"seq": 41, "type": "keyDown", "key": "Return"},
  {"seq": 42, "type": "present", "request": "r-19"},
  {"seq": 43, "type": "focusGained", "request": "r-19"},
  {"seq": 44, "type": "keyUp", "key": "Return"},
  {"seq": 45, "type": "keyDown", "key": "Return"},
  {"seq": 46, "type": "keyUp", "key": "Return"}
]
```

The expected output shape is equally important:

```json
[
  {"seq": 44, "decision": "pending", "reason": "inherited-input"},
  {"seq": 46, "decision": "approved", "request": "r-19"}
]
```

If your implementation approves at sequence 44, it has accepted a press that started before the dialog existed. That is the bug, expressed without a screenshot or a race that appears only once a week.

Build a compact test matrix around state changes rather than around labels. Cover at least these cases:

- A shortcut is down before presentation and released after focus arrives.
- Focus leaves after a fresh press begins and returns before release.
- A repeat event arrives while a request remains pending.
- Request A closes, request B opens, and an old handler fires.
- The vault or other prerequisite changes state after the dialog appears.

For each case, assert both that no action is sent and that the audit trail records a denial or no decision, according to your design. A silent ignored event can be fine. A record that claims approval when the reducer denied it is not.

Then test the UI integration, where many otherwise good reducers get bypassed. Verify that every activation route goes through the same gate: pointer click, Return, Space, accessibility action, programmatic default-button action, and any menu command. A separate shortcut path that calls the action directly will drift from the button path over time.

Use dependency injection for the action executor in these tests. The test should receive a call object only after the reducer has emitted approval for the current token. Count calls, capture the request token, and fail if an old token reaches the executor. Do not test this by sending an actual HTTP request or SSH command. The authorization boundary should be testable without a credential or a network.

Manual testing still matters because operating systems deliver focus and accessibility events through paths that mocks can miss. Record a short regression script in plain language, run it on the oldest supported system and a current one, and perform it with a physical keyboard. Include fast window switching, held keys, repeated keys, system alerts, screen lock, wake from sleep, and a request that is revoked while visible. The script should state the expected decision after each action, not merely say that the dialog behaved correctly.

## A denied event must stay denied

Once an event fails the approval predicate, later UI changes must not rehabilitate it. This sounds obvious until an implementation stores a generic "pending approve" flag and checks it again after focus returns or an animation finishes.

Make rejection terminal for the event. If Return arrives while disarmed, record it as inherited input or discard it, then wait for a later fresh press. If focus changes during a chord, invalidate that chord. If the request changes, invalidate every event and callback associated with the previous request. The code should not keep a candidate approval around in case conditions improve.

This principle also protects against duplicate actions. A double click, a keyboard action followed by a pointer action, or an event replay should not cause two external calls. When the authorization layer accepts one approval token, mark the request consumed before dispatching the action. Subsequent activations for that token should produce a harmless duplicate rejection.

Separate the UI outcome from the external action outcome. The UI can say "approved for dispatch" only after it has closed the decision state. The executor can then succeed or fail independently. If a network request fails, do not reactivate the old approval token and quietly retry on a later keystroke. Ask again if the user must authorize another attempt, especially when the action payload or destination may have changed.

This is where audit design earns its keep. Log enough context to reconstruct that an input was rejected because it was stale, repeated, unfocused, revoked, or already consumed. Avoid turning the log into a capture of every keystroke. You need decision evidence, not a surveillance record.

## Identity, authorization, and scope need separate answers

A trustworthy approval flow answers who asked, what they want to do, and whether the human approves that specific action. It should not use one answer as a substitute for another.

Process identity is useful because it lets the human decide whether the requester is expected. On macOS, code signing authority can give a person a more meaningful signal than a mutable process name. But an expected process can still make an unexpected request, and a human can still approve the wrong request through a stray shortcut.

Scope is separate again. A session permission may be reasonable for low risk work that a human has inspected, but it does not turn every future call into the same decision. Per action confirmation has a different purpose: it forces a fresh judgement at the moment an action uses a credential or reaches a sensitive destination. Design the interaction so the user can see which layer they are changing.

Avoid vague labels such as "Allow" when the request involves an external effect. State the method, destination, and meaningful operation in language the operator can check. Do not turn the dialog into a dense packet dump either. Show the decision-relevant facts first, and make exact details available without making approval depend on a scrolling exercise.

Sallyport keeps these layers intentionally narrow: a locked vault denies every action, a new agent process requests session authorization by default, and selected credentials can require a fresh approval for every use. That design does not make keyboard interaction safe by itself; the shortcut must still prove that the current person approved the current call.

## Ship only after the failure trace is boring

Release the shortcut only when hostile input produces ordinary, explainable denials. A person should be able to hold Return through presentation, switch windows mid-chord, trigger a repeated Space, or race a replacement request without causing an external call.

Put the test fixture in the same review as the UI change. Require a reviewer to read the event sequence that proves a stale key cannot cross the boundary. Require the implementation to expose a single authorization function that checks request identity, focus generation, input freshness, and consumption state. Convenience handlers should call that function, never bypass it.

Watch production logs for patterns that suggest friction without weakening the predicate. Many inherited-input denials after a modal appears may mean the request arrives at an awkward time. Improve when and how the request is presented, or offer a pointer and biometric route. Do not "fix" the metric by accepting the events that the system correctly rejected.

A good approval shortcut feels uneventful after a person has looked at the request. That is the only speed worth optimizing. Every other fast path is just a way to turn terminal muscle memory into authorization.
