pitboard-core 0.8.0

The engine behind Pitboard: parking and restoring Claude Code and Codex logins. Serves Pitboard's own front ends.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
//! Every fact about Claude Code that Pitboard stands on, named and dated.
//!
//! The keychain item's name and how the slot is hashed from a directory, the five keys a
//! logout deletes, the write lock and its constants, the one write that skips the lock, the
//! 4032-byte ceiling on a `security` command, the thirty seconds a session caches a
//! credential for. All of it was read out of one build.
//!
//! None of it transfers. The next provider's equivalents have to be read out of its own
//! build the same way, into a list of its own, dated on its own schedule.
//!
//! See [`crate::assumptions`] for what an entry means and how a probe reads one.

use crate::assumptions::OnSystem::{NotRead, Pending, Read};
use crate::assumptions::{Assumption, OnSystem, PerSystem};

/// The build every entry below was read from on macOS and Linux, unless it says otherwise.
pub const VERIFIED_AGAINST: &str = "2.1.284";

/// The build the facts read on Windows were read from: `@anthropic-ai/claude-code-win32-x64`
/// and `-win32-arm64` 2.1.289, both built from commit 736d26e, as that version's macOS and
/// Linux builds are. Read as bytes on a Mac on 2026-10-06, and never run.
pub const WINDOWS_VERIFIED_AGAINST: &str = "2.1.289";

/// The Linux build has no keychain backend, so a keychain fact read from it reports the
/// keychain gone, as it did on every build from 2.1.278 on.
const NO_KEYCHAIN_ON_LINUX: OnSystem =
    NotRead("Linux has no keychain, and Claude Code's Linux build has no keychain backend");

/// Nor has the Windows build. Both Windows builds of 2.1.289 keep code they share with the
/// macOS one: the fallback wrapper's `keychain_locked_skip_fallback` and
/// `primary_transient_skip_fallback`, and a device key store that names
/// `find-generic-password`. What they lack is the backend itself: none holds `exceeds
/// security -i stdin limit; using argv`, `show-keychain-info` or `[keychain] readAsync
/// failed`, which the macOS build holds twice each.
const NO_KEYCHAIN_ON_WINDOWS: OnSystem =
    NotRead("Windows has no keychain, and Claude Code's Windows build has no keychain backend");

/// A fact read on macOS and Linux from the build its entry names, and on Windows from
/// [`WINDOWS_VERIFIED_AGAINST`].
const fn everywhere(name: &'static str, build: &'static str) -> PerSystem {
    PerSystem {
        name,
        macos: Read(build),
        linux: Read(build),
        windows: Read(WINDOWS_VERIFIED_AGAINST),
    }
}

/// A fact read on macOS and Linux from the build its entry names, whose Windows reading
/// waits on the Windows work.
const fn later_on_windows(
    name: &'static str,
    build: &'static str,
    by: &'static [&'static str],
    reads: &'static str,
) -> PerSystem {
    PerSystem {
        name,
        macos: Read(build),
        linux: Read(build),
        windows: Pending { by, reads },
    }
}

/// What each fact below is on each system. The Windows readings are a reading of the
/// Windows builds' code for each fact, and of every literal its entry probes for, which
/// both Windows builds hold. Where Claude Code does something else on Windows, that is a
/// fact of its own, read by the pull request named.
pub const PER_SYSTEM: &[PerSystem] = &[
    PerSystem {
        name: "credential_service_name",
        macos: Read(VERIFIED_AGAINST),
        linux: NO_KEYCHAIN_ON_LINUX,
        windows: Pending {
            by: &["W23"],
            reads: "credman_target: the Credential Manager target Claude Code keeps the login \
                    under on Windows, once it uses that store",
        },
    },
    PerSystem {
        name: "keychain_account_name",
        macos: Read(VERIFIED_AGAINST),
        linux: NO_KEYCHAIN_ON_LINUX,
        windows: Pending {
            by: &["W22"],
            reads: "credman_account_name: the user name on Claude Code's Credential Manager \
                    item",
        },
    },
    later_on_windows(
        "live_chain_order",
        VERIFIED_AGAINST,
        &["W22", "W23"],
        "windows_backend_choice (W22): which store Claude Code keeps the login in on Windows, \
         and credman_migration (W23): how it moves the login into Credential Manager",
    ),
    PerSystem {
        name: "keychain_write_route",
        macos: Read(VERIFIED_AGAINST),
        linux: NO_KEYCHAIN_ON_LINUX,
        windows: NO_KEYCHAIN_ON_WINDOWS,
    },
    PerSystem {
        name: "keychain_absence_codes",
        macos: Read(VERIFIED_AGAINST),
        linux: NO_KEYCHAIN_ON_LINUX,
        windows: NO_KEYCHAIN_ON_WINDOWS,
    },
    everywhere("write_lock", VERIFIED_AGAINST),
    everywhere("logout_skips_the_lock", VERIFIED_AGAINST),
    everywhere("account_scoped_keys", VERIFIED_AGAINST),
    later_on_windows(
        "credential_cache",
        "2.1.278",
        &["W22", "W23"],
        "windows_file_adoption (W22): how soon a running session takes a new \
         `.credentials.json` on Windows, and windows_credman_cache (W23): how long it keeps a \
         login read from Credential Manager",
    ),
    everywhere("config_file_location", "2.1.289"),
    everywhere("oauth_client", VERIFIED_AGAINST),
    later_on_windows(
        "supervisor_daemon",
        VERIFIED_AGAINST,
        &["W18"],
        "windows_daemon_identity: how the supervisor daemon is told apart from other \
         processes on Windows",
    ),
    PerSystem {
        name: "no_keyring_off_macos",
        macos: NotRead(
            "this is about the systems without a keychain, and macOS keeps the login in one",
        ),
        linux: Read(VERIFIED_AGAINST),
        windows: NotRead(
            "Claude Code's Credential Manager store on Windows calls `Bun.secrets`, which this \
             fact rules out, so read from the Windows build it reports a keyring arrived",
        ),
    },
    everywhere("sign_in_output", "2.1.289"),
    everywhere("sign_in_takes_another_code", "2.1.289"),
    later_on_windows(
        "install_places",
        "2.1.289",
        &["W17"],
        "windows_install_layout: where the native installer and npm put `claude.exe` on \
         Windows",
    ),
    later_on_windows(
        "plaintext_credential_mode",
        VERIFIED_AGAINST,
        &["W22"],
        "windows_plaintext_write: how Claude Code writes `.credentials.json` on Windows, and \
         what keeps others out of it there",
    ),
];

pub const ASSUMPTIONS: &[Assumption] = &[
    Assumption {
        name: "credential_service_name",
        fact: "the login lives in a keychain item named `Claude Code${OAUTH_FILE_SUFFIX}-credentials`, \
               with `-<first 8 hex of sha256 of the NFC-normalised storage directory>` appended for \
               any directory but the default",
        read_from: "the secure storage module's service name and slot derivation",
        verified_against: VERIFIED_AGAINST,
        depends: "slot.rs, and every read and write of the live credential",
        probe: &["-credentials", "OAUTH_FILE_SUFFIX"],
        absent: &[],
    },
    Assumption {
        name: "keychain_account_name",
        fact: "the keychain account is $USER when it matches /^[a-zA-Z0-9._-]+$/, and \
               `claude-code-user` when it does not",
        read_from: "the secure storage module's account name",
        verified_against: VERIFIED_AGAINST,
        depends: "slot::account_name",
        probe: &["claude-code-user"],
        absent: &[],
    },
    Assumption {
        name: "live_chain_order",
        fact: "on macOS the login is read from the keychain first and a plaintext \
               `.credentials.json` second, and a write moves it to the file only when the \
               keychain write fails for good; on Linux the file is the only store. The \
               successor backend replaces the fallback half and only for a caller that hands a \
               backend in, which an ordinary `claude` does not",
        read_from: "the keychain-with-plaintext-fallback store and `getSecureStorage`",
        verified_against: VERIFIED_AGAINST,
        depends: "store::resolve and everything that reads or writes through it",
        probe: &[".credentials.json", "-with-", "-fallback"],
        absent: &[],
    },
    Assumption {
        name: "keychain_write_route",
        fact: "a write gives `security -i` the line `add-generic-password -U -a \"<account>\" \
               -s \"<service>\" -X \"<hex>\"` on stdin while that line is at most 4032 bytes, \
               and the same command on the argument line above that; it never refuses or \
               splits a login. Each call has a 2 second timeout. A failed write is transient, \
               and skips the plaintext fallback, when it timed out or, from 2.1.281, when it \
               exited 36 after this process had seen the item; any other failure moves the \
               login to `.credentials.json`",
        read_from: "the keychain backend's update path and the fallback wrapper around it",
        verified_against: VERIFIED_AGAINST,
        depends: "host::macos::keychain::MAX_COMMAND_BYTES and the write path",
        probe: &[
            "exceeds security -i stdin limit; using argv",
            "primary_transient_skip_fallback",
            "keychain_locked_skip_fallback",
        ],
        absent: &[],
    },
    Assumption {
        name: "keychain_absence_codes",
        fact: "`find-generic-password -a <account> -w -s <service>`, with a 2 second timeout: \
               exit 0 with output is the login; exit 0 with nothing, exit 44, or output that is \
               not JSON is absent. Exit 36, a locked keychain, is absent to an ordinary read, a \
               failed read to a write once this process has seen the item \
               (`failureIfTransient`, from 2.1.281), and a failed read outright only when a \
               caller asks for that. Any other exit is a failed read. `show-keychain-info` \
               exiting 36 only adds an unlock hint. Pitboard reads 36 as unreadable on \
               purpose, the strict end of that",
        read_from: "the keychain backend's read path",
        verified_against: VERIFIED_AGAINST,
        depends: "host::macos::keychain::classify",
        probe: &[
            "failureIfTransient",
            "[keychain] readAsync failed; not caching a null",
            "show-keychain-info",
        ],
        absent: &[],
    },
    Assumption {
        name: "write_lock",
        fact: "every credential write takes proper-lockfile's directory lock at \
               `<storage dir>/.storage-write`, stale 15000ms, ten retries, 100ms to 1000ms of \
               backoff, and re-reads the credential inside the lock before changing it. A \
               re-read that fails abandons the write (`read_failed_skip_write`); from 2.1.281 a \
               locked keychain counts as a failed re-read once this process has seen the item, \
               where before it read as empty",
        read_from: "the secure storage module's write wrapper",
        verified_against: VERIFIED_AGAINST,
        depends: "lock.rs and the whole switch",
        probe: &[
            ".storage-write",
            "[secureStorage] write lock compromised: ",
            "read_failed_skip_write",
            "failureIfTransient",
        ],
        absent: &[],
    },
    Assumption {
        name: "logout_skips_the_lock",
        fact: "`/logout` retries for its own 7.5 seconds and then deletes the credential with \
               no lock held at all",
        read_from: "the secure storage module's already-locked escape hatch",
        verified_against: VERIFIED_AGAINST,
        depends: "the slot re-read in switch, which exists for this",
        probe: &["secureStorage.READ_FAILED"],
        absent: &[],
    },
    Assumption {
        name: "account_scoped_keys",
        fact: "signing in again deletes claudeAiOauth, organizationUuid, trustedDeviceToken, \
               enterpriseGateway and designOauth, and `/logout` clears the whole document but \
               writes back `coworkRemoteDevice`, so those five belong to the account",
        read_from: "the re-login prune and the logout path",
        verified_against: VERIFIED_AGAINST,
        depends: "switch::ACCOUNT_SCOPED, and what a park holds",
        probe: &[
            "claudeAiOauth",
            "organizationUuid",
            "trustedDeviceToken",
            "enterpriseGateway",
            "designOauth",
        ],
        absent: &[],
    },
    Assumption {
        name: "credential_cache",
        fact: "a running session serves the credential from a 30 second cache, so a swap is \
               picked up within about 33 seconds. The 30 seconds are unchanged in 2.1.284; its \
               re-checks after 1, 3 and 10 seconds, behind `tengu_streamed_thimble` from \
               2.1.281, can only shorten that",
        read_from: "the keychain backend's cache, and measured against a running session",
        // The 33 seconds were measured against a running 2.1.278; nothing later was run.
        verified_against: "2.1.278",
        depends: "switch::ADOPTION_CEILING_SECONDS, and autoswitch, which switches before a \
                  limit rather than at it so a session already running follows in time",
        probe: &[],
        absent: &[],
    },
    Assumption {
        name: "config_file_location",
        fact: "a legacy `<config dir>/.config.json` wins when present; otherwise \
               `<$CLAUDE_CONFIG_DIR or the home folder>/.claude<suffix>.json`. The two read \
               `CLAUDE_CONFIG_DIR` differently: the file's base is `CLAUDE_CONFIG_DIR || \
               homedir()`, so an empty value is the home folder, while the config dir is \
               `(CLAUDE_CONFIG_DIR ?? join(homedir(), \".claude\")).normalize(\"NFC\")`, so an \
               empty value makes it the empty path and `.config.json` is looked for in the \
               working directory",
        read_from: "the config path resolution, and the config dir the secure storage module \
                    shares",
        // Read on 2026-10-06 from the macOS, Linux and Windows builds of 2.1.289, x64 and
        // arm64, whose code for both paths is the same.
        verified_against: "2.1.289",
        depends: "claude::config_file and claude::config_dir, which read CLAUDE_CONFIG_DIR \
                  the two ways, and home::check_absolute, which refuses an empty one",
        probe: &[".config.json", "CLAUDE_CONFIG_DIR"],
        absent: &[],
    },
    Assumption {
        name: "oauth_client",
        fact: "every login Claude Code stores was issued to client 9d1c250a-e61b-44d9-88ed-5944d1962f5e, \
               and a refresh answer without a refresh-token lifetime keeps the one it had",
        read_from: "the OAuth client id and the token refresh path",
        verified_against: VERIFIED_AGAINST,
        depends: "api::CLIENT_ID and park::renewed",
        probe: &["9d1c250a-e61b-44d9-88ed-5944d1962f5e"],
        absent: &[],
    },
    Assumption {
        name: "supervisor_daemon",
        fact: "a supervisor daemon outlives the session that started it, refreshes the login on \
               a timer of roughly eight hours, records itself in `<config dir>/daemon.lock`, and \
               writes through the same lock as everything else",
        read_from: "the daemon's auth scheduler and its lock file",
        verified_against: VERIFIED_AGAINST,
        depends: "daemon.rs, and the lock discipline the switch relies on",
        probe: &[
            "daemon.lock",
            "daemon.status.json",
            "auth: scheduling proactive refresh in ",
        ],
        absent: &[],
    },
    Assumption {
        name: "no_keyring_off_macos",
        fact: "Claude Code's code has no Secret Service, libsecret, gnome-keyring or KWallet \
               backend. Its storage backends are `keychain`, `plaintext` and `windows-credman`, \
               the last behind the `tengu_windows_credman` flag, and on Linux it uses \
               `plaintext` alone, so the login is the file. The `libsecret` in the Linux binary \
               belongs to the Bun runtime it ships in, behind `Bun.secrets`, which Claude \
               Code's code never calls",
        read_from: "the secure storage module's backend list and `getSecureStorage`, and the \
                    whole build searched for every Linux keyring name",
        verified_against: VERIFIED_AGAINST,
        depends: "host::linux, and Pitboard's claim that a parked login on Linux is no \
                  less protected than the live one",
        probe: &[
            "tengu_windows_credman",
            "CLAUDE_CODE_FORCE_WINDOWS_CREDMAN",
            r#"["keychain","plaintext","windows-credman"]"#,
        ],
        absent: &[
            "Bun.secrets",
            "org.freedesktop.secrets",
            "gnome-keyring",
            "SecretService",
        ],
    },
    Assumption {
        name: "sign_in_output",
        fact: "`claude auth login` writes `Opening browser to sign in…`, then `If the browser \
               didn't open, visit: <address>`, then `Paste code here if prompted > ` with no \
               newline, all to stdout and before it opens the browser, and from then on reads \
               a pasted `<code>#<state>` line from stdin. The address is the manual one: \
               `https`, on claude.com for a claude.ai login and platform.claude.com for a \
               Console one, coming back to platform.claude.com's page that shows the code. The \
               browser it opens goes to another address, which comes back to the loopback. \
               The address goes through a hyperlink helper, which writes it bare, or, where \
               its check says the terminal takes hyperlinks, as an OSC 8 hyperlink: \
               `ESC ] 8 ; ;`, the address, BEL, the address again as the link's text, bright \
               blue where colour is on, then `ESC ] 8 ; ;` and BEL. Piped, the check says yes \
               when `FORCE_HYPERLINK` is set to anything but 0 or nothing, which decides it \
               when set; otherwise when `NETLIFY` is set, `TERM_PROGRAM` or `LC_TERMINAL` is \
               ghostty, Hyper, kitty, alacritty, iTerm.app, iTerm2 or WarpTerminal, \
               `TERMINAL_EMULATOR` is JetBrains-JediTerm, `WT_SESSION` is set outside tmux, \
               `TERM_PROGRAM` is tmux 3.4 or later, or `TERM` contains kitty. A terminal \
               attached to a background session answers for the check instead, and \
               `auth login` has none",
        read_from: "the `auth login` command's OAuth flow and `startOAuthFlow`, the authorize \
                    address builder and its constants, the hyperlink helper the address is \
                    printed through with `assumeSupport`, and the supports-hyperlinks check \
                    it asks",
        // Read from a newer build than the rest of this register.
        verified_against: "2.1.289",
        depends: "provider::claude::engine's read_sign_in, provider::printed, which reads \
                  the hyperlink, and the address and code field the app's sign-in sheet \
                  offers",
        probe: &[
            "If the browser didn't open, visit: ",
            "Paste code here if prompted > ",
            r#"CLAUDE_AI_AUTHORIZE_URL:"https://"#,
            r#"CONSOLE_AUTHORIZE_URL:"https://"#,
            r#"MANUAL_REDIRECT_URL:"https://"#,
            // A sign-in address going through the hyperlink helper with `assumeSupport`, from
            // after the helper's name, which the minifier chooses. `auth login` makes one of
            // the three such calls in 2.1.289; 2.1.110 has none.
            "{assumeSupport:!0})}",
        ],
        absent: &[],
    },
    Assumption {
        name: "sign_in_takes_another_code",
        fact: "`claude auth login` reads each line typed back while it waits, trims it and \
               splits it at `#`. A line that is not `<code>#<state>` with both halves it \
               refuses by writing `Invalid code. Please make sure the full code was copied.` \
               and a newline to stderr, and it goes on reading in the same process, printing \
               no prompt again, so another line can be typed back. The first line with both \
               halves it takes, whatever its state half says. After that it still refuses a \
               line without both halves, and ignores one with them; a code the token exchange \
               then refuses ends the sign-in with `Login failed: ` on stderr and exit status 1",
        read_from: "the `auth login` command's handler for each line read from stdin, which \
                    trims and splits the line and refuses it before it looks at whether a code \
                    was taken, returns after the refusal and leaves the line reader open, and \
                    the OAuth service's `waitForAuthorizationCode` and \
                    `handleManualAuthCodeInput`, which takes a code only while its resolver is \
                    set, hands on the code half alone and clears the resolver",
        // Read on 2026-10-05 from the Windows arm64 build's sources and the macOS and Linux
        // builds of 2.1.289, all of which hold every literal below; 2.1.110 holds none.
        verified_against: "2.1.289",
        depends: "provider::claude::engine's refused_code, and the app model offering the \
                  code field again once Claude Code has refused a code",
        probe: &[
            // The refusal, then the handler returning rather than ending the process.
            "Invalid code. Please make sure the full code was copied.\n`);return}",
            "if(this.manualAuthCodeResolver)this.authorizationCodeReceived=!0",
        ],
        absent: &[],
    },
    Assumption {
        name: "install_places",
        fact: "the native installer puts its launcher at `~/.local/bin/claude`, and says so \
               when that directory is not on `PATH`; a global npm install puts `claude` in \
               npm's global `bin`, which is in Homebrew's prefix, `/opt/homebrew` or \
               `/usr/local`, or in `/usr/local` where nodejs.org's installer put Node, and \
               Claude Code tells it is one by `/node_modules/@anthropic-ai/` in the path the \
               running program resolves to; one whose path runs through a Homebrew \
               `Caskroom` is the Homebrew cask's",
        read_from: "the installation-method detection, which returns `npm-global` for an \
                    `execPath` holding `/node_modules/@anthropic-ai/` and lists \
                    `/opt/homebrew/bin` and `/usr/local/bin` among npm's places, \
                    `getHomebrewCaskName`, and the native install's PATH advice",
        // Read on 2026-10-05 from the macOS builds of 2.1.283 and 2.1.289 and the Linux build
        // of 2.1.289, all of which hold every literal below.
        verified_against: "2.1.289",
        depends: "claude::paths::install_places, where an app looks for `claude` after the \
                  login shell's PATH",
        probe: &[
            "Native installation exists but ~/.local/bin is not in your PATH",
            ".local/bin/claude",
            "/node_modules/@anthropic-ai/",
            "Detected Homebrew cask installation: ",
        ],
        absent: &[],
    },
    Assumption {
        name: "plaintext_credential_mode",
        fact: "the plaintext credential is written and then chmod'd to 0600, in its storage \
               directory, under the fixed name `.credentials.json`",
        read_from: "the plaintext backend's write path, which chmods 384 after writing",
        verified_against: VERIFIED_AGAINST,
        depends: "store::file, slot::CRED_FILE, and atomic::Perms::Secret matching what \
                  Claude Code itself writes",
        probe: &[
            ".credentials.json",
            "Warning: Storing credentials in plaintext.",
        ],
        absent: &[],
    },
];

#[cfg(test)]
mod tests {
    use super::*;
    use crate::assumptions::Platform;

    /// The facts the Windows builds of 2.1.289 were read for, and those they were not, by
    /// name. Read on Windows, the keychain facts would report the keychain gone, and
    /// `no_keyring_off_macos` a keyring arrived.
    #[test]
    fn windows_reads_the_facts_its_build_was_read_for() {
        let read: Vec<&str> = PER_SYSTEM
            .iter()
            .filter(|line| matches!(line.on(Platform::Windows), Read(_)))
            .map(|line| line.name)
            .collect();
        assert_eq!(
            read,
            [
                "write_lock",
                "logout_skips_the_lock",
                "account_scoped_keys",
                "config_file_location",
                "oauth_client",
                "sign_in_output",
                "sign_in_takes_another_code",
            ]
        );
        for name in [
            "keychain_write_route",
            "keychain_absence_codes",
            "no_keyring_off_macos",
        ] {
            let line = PER_SYSTEM.iter().find(|l| l.name == name).unwrap();
            assert!(matches!(line.on(Platform::Windows), NotRead(_)), "{name}");
        }
    }
}