A file manager whose Ctrl+C cannot be pasted in Explorer is not finished. Until August 2026, cwdio’s was exactly that: the clipboard was a variable inside a React hook. This is how it became the real Windows file clipboard — and the one rule learned along the way that applies to any program touching the clipboard from a process it might kill. Retold from ADR 0031.
What a Windows file copy actually is
Not text. When Explorer copies files it puts three formats on the clipboard together:
| Format | Carries |
|---|---|
CF_HDROP (standard format 15) | a DROPFILES header and a double-null-terminated list of paths |
Preferred DropEffect (registered) | a DWORD: copy (Explorer writes 5, COPY|LINK) or move (2) |
FileNameW / FileName (registered) | a legacy single path, also published |
The second one is what makes a cut a cut. And it is the only channel through which cwdio can hand another application a move: native drag-out (ADR 0030) is copy-only by Electron’s construction and never reports where the drop landed.
Ruled out: Electron alone
Measured on Electron 42, not inferred:
clipboard.writeBuffer("CF_HDROP", …)registers the string"CF_HDROP"as a custom format instead of using standard format 15. The shell never sees it.- Two formats cannot coexist through
writeBuffer: every call clears the clipboard first, so writing the path list and then the effect leaves only the effect. - Electron can read exactly one path back (
FileNameW); a multi-file copy from Explorer is unreadable.
So writing the file clipboard, and reading its full path list, needs an out-of-process helper. Observation,
however, is free: availableFormats() contains text/uri-list if and only if real files are on the
clipboard (plain text, URLs, bookmarks and HTML all report otherwise), and the effect and any owner marker
can be read with readBuffer. That split shaped everything: enabling Paste costs nothing; only a real
copy, cut or paste spawns a process.
What shipped
cwdio-clipboard.exe — a one-shot C# helper compiled by the in-box .NET Framework csc.exe, the same
no-toolchain pattern four earlier helpers use — with two verbs: read, and write <copy|move> [token]. It
calls SetDataObject(data, copy: true), which flushes the payload so it survives the process exiting. Copy,
cut and paste are human-scale events, so a short-lived process per event costs nothing and leaves no state
to go stale.
Ownership is a token, not a flag. Every write stamps a fresh nonce into a registered
cwdio-clipboard-owner format. On paste, the renderer compares it with the token it last wrote:
- match → the clipboard still holds our payload, so paste the in-app rows, which carry each row’s real
URI — a copy from an
sftp://orarchive://panel still works; - anything else → foreign; paste the OS path list.
It has to be a token because cwdio is multi-window and each window owns its own in-app clipboard. A bare “is this cwdio’s?” marker would make window B recognise window A’s payload as its own, consult its own empty clipboard and paste nothing. With a token, A’s payload reads as foreign in B and pastes from the path list — cross-window copy/paste works because of the distinction.
A selection with no OS paths still writes the token, with an empty list. A remote selection has nothing to hand the shell, but the copy must still replace the clipboard, or a stale Explorer payload would outrank the files you just copied and Ctrl+V would paste the wrong thing.
A consumed cut is retired, as Explorer does after a cut-paste — by writing an empty owned payload, and only after the paste was actually dispatched. Both clipboards, in-app and OS, retire through one gate; during development the in-app half kept an older gate and Explorer could paste a spent cut whose sources had already moved. That is now an end-to-end test, not a memory.
Foreign paths are canonicalised to file:// URIs so the “paste into its own folder” guard compares
like with like, and a foreign selection that spans folders — an Explorer search result — is transferred one
source directory at a time so a move’s undo returns each item to its own origin.
Never hold the clipboard open
The most transferable lesson in the ADR. The obvious way to retire a cut is to empty the clipboard, and both obvious implementations are wrong:
- Raw
OpenClipboard→EmptyClipboard→CloseClipboardholds the clipboard open across our own code, andEmptyClipboardblocks while it notifies the previous owner. Kill the helper on a timeout inside that window and the clipboard is left open with no live owner — wedged for the whole machine. Every application’s Ctrl+C stops working until Explorer is restarted. This happened twice on the development machine and was first misdiagnosed as an environment problem. Clipboard.Clear()isSetDataObject(new DataObject())withcopy: false, so the empty object is never flushed and is owned by a process that exits immediately. Observable state becomes a race — which showed up as a flaky test.
So there is no clear operation at all. Every mutation goes through the one flushed, self-contained
SetDataObject(data, copy: true); “retire this payload” is just a write with an empty list. One primitive,
one failure mode, verified deterministic over repeated rounds while Electron’s main thread was deliberately
blocked — the exact condition that deadlocked the raw version.
What it deliberately does not do
- It does not run on macOS or Linux yet. There the file clipboard is absent, not broken: the handlers no-op rather than reject, or every Ctrl+C would raise an error for a capability the build never claimed. A missing helper on Windows does report, because there the copy really did fail to reach Explorer.
- It does not implement “Performed DropEffect”, so after cwdio pastes an Explorer cut, Explorer’s source view stays dimmed until it notices the files are gone. Retiring the clipboard covers the correctness half; the cosmetic half self-resolves.
- It does not stage remote or in-archive selections to a temp folder so they can leave the app. That is the same limitation as drag-out, for the same reason, and the same future mechanism would serve both.
Every mechanic was measured before it was built: a separate process reads the helper’s write back intact;
Explorer’s own Paste verb accepted a folder, a name with a space and 日本語 файл.txt; PowerShell’s
Set-Clipboard -Path and Get-Clipboard -Format FileDropList interoperate in both directions. The
user-facing summary is on the Windows integration page.