Shell Pilot 1.22.1

September 6, 2026 · Bug-fix release

Two fixes: the microphone finally works, and the ambient reflection no longer freezes on a colour the terminal has stopped showing. Both are fixes to things 1.22.0 shipped — the second one has never reached anyone before now.

Microphone access

The symptom

Dictation did nothing. Press the hotkey or the toolbar mic button and no text arrived, with no error worth acting on.

The more useful half of the symptom was somewhere else: Shell Pilot never appeared in System Settings ▸ Privacy & Security ▸ Microphone at all — not as an allowed app, and not as a denied one either. That absence is the diagnosis. An app that has asked for the microphone and been refused is still listed in that pane with its switch off, so you can change your mind. An app that is missing from the list entirely was never asked about, which means the request never got as far as the part of macOS that asks. There was nothing for the user to switch on, and no amount of clicking in System Settings could have fixed it.

Root cause

Shell Pilot runs under Hardened Runtime. Hardened Runtime is deny-by-default over the resources the OS guards: a process may reach one only if the matching entitlement is present in the code signature. The entitlement for the microphone is com.apple.security.device.audio-input, and it was not there.

So the audio-input request was refused by the runtime before it ever reached TCC — the subsystem that owns privacy permissions and draws the prompts. And TCC only creates a record for an app when it is actually consulted about one. It was never consulted, so no record was created, so no row appeared in System Settings. The two failures people saw were one failure: the app was stopped a layer earlier than the layer that would have told anyone about it.

The usage-description strings in Info.plist — NSMicrophoneUsageDescription and NSSpeechRecognitionUsageDescription — were correct the entire time, which is part of why this was easy to miss. A usage string only supplies the wording of a prompt. The entitlement is what makes the prompt possible at all.

The release-process half, which is the half that mattered

Declaring the entitlement was necessary, and on its own it would have changed nothing a user could see.

Release builds are produced with CODE_SIGNING_ALLOWED=NO and signed afterwards by hand, and the documented signing step never passed --entitlements. The consequence is worth stating plainly: the project's entitlements file was not consulted at any point on the path to a shipped build. The command that produced every release — codesign --force --deep --options runtime --sign … — applied hardened runtime and no entitlements whatsoever.

This is confirmed rather than inferred. The shipped v1.22.0 DMG reports flags=0x10000(runtime) over an empty entitlements dictionary. So every release built under this process has carried the strict half of the pair and none of the exemptions — including com.apple.security.cs.allow-unsigned-executable-memory and com.apple.security.cs.disable-library-validation, which the project has declared since SwiftTerm needed them and which have quietly been absent from the signature ever since.

Flipping the build setting and editing the plist while leaving the signing command alone would have produced a 1.22.1 that behaved exactly like 1.22.0.

What changed

On first dictation macOS now prompts for the microphone, and Shell Pilot appears in System Settings ▸ Privacy & Security ▸ Microphone whether you allow it or deny it.

The ambient reflection no longer freezes

Why this is in a patch release

This fix was written as a correction to 1.22.0 and has never been deployed. The 1.22.0 DMG was built and published about an hour before the fix landed, so the 1.22.0 that people are running is the ambient reflection with the freezing bug in it. If you are on 1.22.0, this is the first build in which the reflection behaves as it was described.

The bug

Sampling was armed only by terminal output. The last pass of a burst runs 80 ms after the last byte — often before the screen has finished settling, as a pager repaints, a progress bar erases itself, or a program clears on exit. After that pass, nothing ever sampled again, because nothing ever wrote again.

Whatever that final pass happened to catch was then frozen on the sidebar indefinitely: a hard red wash still glowing beside a terminal that had long since gone back to a bare prompt.

The fix

A second cadence that needs no output. Any pass that still has somewhere to converge arms another one 350 ms later — roughly the smoothing constant's own time scale, fast enough that a blank screen fades out in about two seconds, slow enough that idling on a coloured screen costs the 1200-cell walk three times a second rather than twelve.

The part that matters is how it stops. The chain ends only when the smoothed value and the published value are both exactly neutral, which the fade-out reaches because it snaps to neutral rather than decaying asymptotically. So an idle shell leaves no timer running at all — which is the half a plain repeating timer would get wrong, since polling forever to catch a rare change is precisely the cost this sampler exists to avoid.

Exactly one pass is in flight at a time. Output pre-empts a pending settle pass; a settle pass pre-empts nothing. And the early-outs for "no window on screen" and "terminal not sized yet" now re-arm instead of returning bare, so hiding the app with ⌘H in the middle of a build no longer strands the wash on the colour it had at the time — nothing is walked while there is no window to light, but a pass stays armed, and the wash resumes converging the moment the app is back.

Tests

371 tests pass across 29 suites, unchanged from 1.22.0. Nothing in this release touches Swift logic — it is a build setting, an entitlements plist and a signing flag. The stop condition for the settle cadence is covered by the tests added alongside it, as a pure static function so it can be exercised without a live terminal, a window and a run loop.

Known limits