← back to Secrets Manager
TK-11683: harden transcript scanner read loop (mid-write race) — retry-read once + exclude ENOENT-vanished files from unreadable, add fail-safe negative test
11e21a4aa6b59a6c05be27de9f33b05a31972545 · 2026-09-16 12:29:22 -0700 · Steve Abrams
A live transcript caught mid-write or rotated away between find-enumeration and
read (TK-11795) flipped the whole canary to WARN 'cannot certify clean'. Now:
retry the read once after a 150ms sync sleep for the transient case; a VANISHED
file (ENOENT — no longer on disk, so it cannot hold a persistent secret) is
excluded rather than counted as an unmeasured gap; a genuinely-unreadable
(non-ENOENT) file surviving the retry still counts unreadable -> WARN. Negative
test extended to prove the fail-safe direction holds (EACCES file -> WARN).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qf3DHSrZSGBXkNENgsYdM8
Files touched
Diff
commit 11e21a4aa6b59a6c05be27de9f33b05a31972545
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Sep 16 12:29:22 2026 -0700
TK-11683: harden transcript scanner read loop (mid-write race) — retry-read once + exclude ENOENT-vanished files from unreadable, add fail-safe negative test
A live transcript caught mid-write or rotated away between find-enumeration and
read (TK-11795) flipped the whole canary to WARN 'cannot certify clean'. Now:
retry the read once after a 150ms sync sleep for the transient case; a VANISHED
file (ENOENT — no longer on disk, so it cannot hold a persistent secret) is
excluded rather than counted as an unmeasured gap; a genuinely-unreadable
(non-ENOENT) file surviving the retry still counts unreadable -> WARN. Negative
test extended to prove the fail-safe direction holds (EACCES file -> WARN).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qf3DHSrZSGBXkNENgsYdM8
---
scan-transcripts.mjs | 57 ++++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 53 insertions(+), 4 deletions(-)
diff --git a/scan-transcripts.mjs b/scan-transcripts.mjs
index 22920ee..e5f9ff8 100644
--- a/scan-transcripts.mjs
+++ b/scan-transcripts.mjs
@@ -48,17 +48,42 @@ function runScan(projectsDir) {
const patterns = loadPatterns();
const findings = [];
const hcSeen = new Set(); // distinct high-confidence secret digests
- let scanned = 0, unreadable = 0;
+ let scanned = 0, unreadable = 0, vanished = 0;
const unreadableFiles = [];
const measurable = fs.existsSync(projectsDir);
const files = measurable ? (listTranscripts(projectsDir) || []) : [];
const enumOk = measurable && files !== null;
+ // Synchronous short sleep (no busy-spin) — used only on the rare retry path.
+ const sleepMs = (ms) => {
+ try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); }
+ catch { const until = Date.now() + ms; while (Date.now() < until) {} }
+ };
+
+ // Read every transcript. A LIVE transcript can be caught mid-write or rotated
+ // between enumeration (find) and read (TK-11795): the first read then fails.
+ // Two failure modes, handled distinctly so neither NOISES the canary nor opens
+ // a false-green:
+ // • transient (file briefly locked / partial) → retry ONCE after a short
+ // delay; a single retry clears the mid-write race.
+ // • VANISHED (ENOENT — the file was deleted/rotated away) → it no longer
+ // exists on disk, so it cannot hold a persistent secret. Excluding it is
+ // NOT a measurement gap (nothing to certify), so it does NOT flip to WARN.
+ // A genuinely-unreadable file (a real, non-ENOENT error surviving the
+ // retry) still counts as unreadable → WARN (fail-safe, cannot certify clean).
for (const f of files) {
let text;
try { text = fs.readFileSync(f, 'utf8'); scanned++; }
- catch { unreadable++; unreadableFiles.push(f); continue; }
+ catch (e1) {
+ if (e1 && e1.code === 'ENOENT') { vanished++; continue; }
+ sleepMs(150);
+ try { text = fs.readFileSync(f, 'utf8'); scanned++; }
+ catch (e2) {
+ if (e2 && e2.code === 'ENOENT') { vanished++; continue; }
+ unreadable++; unreadableFiles.push(f); continue;
+ }
+ }
scanText(f, text, patterns, findings, hcSeen);
}
@@ -100,7 +125,8 @@ function runScan(projectsDir) {
projectsDir,
population: files.length, // total transcripts discovered
scanned, // successfully read
- unreadable, // NOT MEASURED
+ unreadable, // NOT MEASURED (real read error)
+ vanished, // deleted/rotated mid-scan — excluded, not a gap
findings_total: findings.length,
high_confidence: hcFindings.length,
distinct_hc_secrets: hcSeen.size,
@@ -145,12 +171,35 @@ function runTest() {
fs.rmSync(tmp, { recursive: true, force: true });
- const pass = dirtyFlagged && !cleanFlagged && r.verdict === 'FAIL' && !rawLeaked;
+ // Scenario 2 (TK-11795 fail-safe direction) — a GENUINELY unreadable transcript
+ // (EACCES, exists but can't be read) with NO secret must STILL yield WARN, never
+ // a silent PASS. Proves the retry-then-count-unreadable path preserves the
+ // "cannot certify clean" fail-safe after the mid-write-race fix.
+ let unreadableIsWarn = true, unreadableCounted = true;
+ try {
+ const tmp2 = fs.mkdtempSync(path.join(os.tmpdir(), 'tk11683-negtest2-'));
+ const sub2 = path.join(tmp2, '-Users-fake-Projects-perm');
+ fs.mkdirSync(sub2, { recursive: true });
+ fs.writeFileSync(path.join(sub2, 'ok.jsonl'), JSON.stringify({ type: 'user', text: 'nothing secret here' }) + '\n');
+ const locked = path.join(sub2, 'locked.jsonl');
+ fs.writeFileSync(locked, JSON.stringify({ type: 'user', text: 'placeholder' }) + '\n');
+ fs.chmodSync(locked, 0o000); // EACCES on read (non-ENOENT) — a real read error
+ const r2 = runScan(tmp2);
+ unreadableIsWarn = r2.verdict === 'WARN';
+ unreadableCounted = r2.unreadable === 1 && (r2.vanished ?? 0) === 0;
+ try { fs.chmodSync(locked, 0o644); } catch {}
+ fs.rmSync(tmp2, { recursive: true, force: true });
+ } catch { unreadableIsWarn = false; unreadableCounted = false; }
+
+ const pass = dirtyFlagged && !cleanFlagged && r.verdict === 'FAIL' && !rawLeaked &&
+ unreadableIsWarn && unreadableCounted;
console.log('NEGATIVE TEST');
console.log(` flags injected fake secret: ${dirtyFlagged ? 'YES ✓' : 'NO ✗'}`);
console.log(` ignores clean/placeholder line: ${!cleanFlagged ? 'YES ✓' : 'NO ✗ (false positive!)'}`);
console.log(` verdict on injected fault: ${r.verdict} ${r.verdict === 'FAIL' ? '✓' : '✗'}`);
console.log(` report contains NO raw secret: ${!rawLeaked ? 'YES ✓' : 'NO ✗ (LEAK!)'}`);
+ console.log(` genuinely-unreadable → WARN: ${unreadableIsWarn ? 'YES ✓' : 'NO ✗ (false green!)'}`);
+ console.log(` unreadable counted (not vanished): ${unreadableCounted ? 'YES ✓' : 'NO ✗'}`);
console.log(pass ? 'RESULT: PASS' : 'RESULT: FAIL');
process.exit(pass ? 0 : 1);
}
← 7c0d555 auto-data-snapshot: 2026-09-16T10:07:35 (1 data files) — reg
·
back to Secrets Manager
·
TK-11683: document ENOENT-exclusion tradeoff (codex-check/Gr b0935a9 →