I Made Operators Retype a Random 24-Character ID They Couldn't Even Select

There’s a particular flavor of embarrassment that comes from reading a bug report and realizing the bug is not a bug. It’s a design decision. Yours. Made carefully, defended in review, locked in with a test.

That happened to me this week.

What I shipped

We have a rule in our web UI that irreversible actions require type-to-confirm. You want to delete a workspace, you type the workspace name. Delete a branch, type the branch name. It’s a well-worn pattern and it works because the value you’re typing is something you recognize — you had to know it to get here.

Stopping a session got the same treatment. The problem is that sessions don’t have human names. They have a generated collision-resistant ID, 24 characters of base36-ish noise. That’s their only identity. So when I wired up the confirmation, I made it ask for the ID.

Then I made it worse. Because the dialog’s object line already shows the machine and workspace, I decided the ID should appear in exactly one place: the label on the confirmation input. Single source of truth, no duplication, clean. I wrote a test asserting that the value appears in the label, so nobody could accidentally move it.

Here is what an operator actually experiences. They click Stop. A dialog appears asking them to type k7m2xq9... — twenty-four characters. They go to select it and copy it, because obviously nobody is transcribing that by hand. The selection vanishes the instant they release the mouse.

So they retype it. Character by character. Squinting. To stop a session, which is roughly as destructive as killing a process.

Why the selection disappeared

Clicking a <label for="..."> activates the input it points at. That’s the whole point of the association — it’s an accessibility feature, and a good one. But activating the input moves focus into it, and moving focus collapses whatever selection the user just made in the label text. Drag to highlight, release, focus jumps, highlight gone. Double-click, same thing.

I reproduced this with a bare label in a headless browser matrix. It failed in WebKit. It passed in Chromium and Firefox. I looked at that result and concluded it was a Safari quirk.

Then the report came in that it also happens in Firefox.

Which means my reproduction was wrong, not the report. A bare label in isolation is not the dialog. The real dialog sits inside a modal and an action menu, and the session list behind it polls on an interval. Every poll re-renders the tree, and re-rendering re-runs focus management. That’s a second, engine-independent way to lose a selection, and my minimal repro had stripped it out entirely.

I still haven’t fully confirmed that mechanism — it needs to be checked against the live component, not a synthetic one. But the shape of the evidence points there, and the important part is that I’d already declared two browsers clear on the basis of a test that couldn’t have caught it.

The part that stings most

The confirmation isn’t even verified anywhere but the client. The stop request doesn’t carry the typed value. The route that handles it has no mismatch check. So the entire ceremony — the dialog, the unselectable ID, the manual transcription — is a speed bump in the browser and nothing more. A determined user could bypass it with two lines in a console.

I built friction that protects nothing and costs everything.

What I should have done

Not asked for the ID at all. Our own convention says type-to-confirm is for actions where the effect can’t be recovered. Stopping a session is recoverable; you start another one. The sibling action for stopping a shell has no typed confirmation, and I didn’t notice I’d diverged from it. The fix is to drop the requirement, or if we want a speed bump, ask for the workspace name the dialog is already displaying — a value the operator actually knows.

Rendered any copyable value outside the label. If text exists so the user can copy it, it must not live inside a <label for>. Put it in a <code> element with user-select: all and associate the input with aria-labelledby pointing at a plain <span>. You keep the accessible name, you lose the focus theft. My label test was enforcing the exact thing that broke it, which is a good reminder that a passing test only tells you the code does what you asked, not that what you asked for was sensible.

Tested the actual dialog in the actual browsers. Our suite runs in jsdom and Chromium. Neither can see this class of bug. Destructive-action dialogs are exactly where cross-engine differences hurt, because they’re the flows where a confused user does something irreversible or gives up on the tool.

What I’m taking away

Type-to-confirm is a comprehension check, not a reflex test. It works when the token means something to the person typing it. A generated identifier means nothing to anyone — it doesn’t confirm understanding, it just confirms manual dexterity. If an object has no human-readable name, that’s a signal the pattern doesn’t apply, not an invitation to substitute its primary key.

And the methodology lesson, which I think is the more durable one: a reduced reproduction that passes does not clear anything. It tells you that that specific arrangement doesn’t fail. When I built a bare label and saw green in Firefox, the correct conclusion was “my repro is missing something,” not “Firefox is fine.” Minimal repros are for confirming a cause you already suspect. They are not for ruling out environments. Reproduce in the real component first, then reduce.

I’ve also started asking a question I should have asked at design time: if this confirmation isn’t enforced on the server, what is it for? If the answer is “slowing people down,” the friction needs to be proportional and the value needs to be recognizable. Mine was neither.