From 009f1fca61a8e0a374cdd87510329ac493c586ca Mon Sep 17 00:00:00 2001 From: "Sharad." Date: Wed, 16 Sep 2026 23:57:40 +0530 Subject: [PATCH 1/2] fix: surface unreadable files as estimated tokens instead of 0 --- src/scan.js | 5 ++++- test/ctxtrim.test.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/scan.js b/src/scan.js index 0579e90..23a67b3 100644 --- a/src/scan.js +++ b/src/scan.js @@ -22,7 +22,10 @@ function fileInfo(abs, size) { closeSync(fd); text = buf.toString("utf8", 0, bytesRead); } - } catch { return { tokens: 0, sample: "" }; } + } catch { + // Unreadable (e.g. EACCES): don't silently drop it — surface an estimate from the known size. + return { tokens: Math.ceil((size || 0) / 4), sample: "" }; + } let tokens = estimateTokens(text); if (bytesRead && bytesRead < size) tokens = Math.round(tokens * (size / bytesRead)); // scale partial reads return { tokens, sample: text.slice(0, 4000) }; diff --git a/test/ctxtrim.test.js b/test/ctxtrim.test.js index e3c856b..2f64198 100644 --- a/test/ctxtrim.test.js +++ b/test/ctxtrim.test.js @@ -122,6 +122,37 @@ test("scan includes symlinked source files", (t) => { assert.deepEqual(result.files.map((file) => file.rel).sort(), ["linked.py", "real.py"]); }); +test("scan surfaces unreadable (EACCES) files with estimated tokens, not silent 0", (t) => { + const root = mkdtempSync(join(tmpdir(), "ctxtrim-unreadable-")); + t.after(() => rmSync(root, { recursive: true, force: true })); + + writeFileSync(join(root, "locked.txt"), "x".repeat(200_000)); + + const readFileSync = fs.readFileSync; + t.mock.method(fs, "readFileSync", (abs) => { + if (String(abs).endsWith("locked.txt")) { + const err = new Error("EACCES: permission denied"); + err.code = "EACCES"; + throw err; + } + return readFileSync(abs); + }); + syncBuiltinESMExports(); + + let result; + try { + result = scanRepo(root); + } finally { + t.mock.restoreAll(); + syncBuiltinESMExports(); + } + + const file = result.files.find((f) => f.rel === "locked.txt"); + assert.ok(file, "unreadable file should be present in the scan"); + assert.ok(file.tokens > 0, `unreadable file silently reported as 0 tokens, got ${file.tokens}`); + assert.ok(result.totals.totalTokens >= file.tokens, "unreadable file should count toward totals"); +}); + test("scan finds trimmable bloat and keeps source", () => { const s = scanRepo(repo); assert.ok(s.totals.trimTokens > 0); From 70a1b2f723b7fb1fc7e773961e1b87573e4d5751 Mon Sep 17 00:00:00 2001 From: "Sharad." Date: Thu, 17 Sep 2026 08:20:47 +0530 Subject: [PATCH 2/2] chore: bump version to 0.1.15 and add changelog entry --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a3e40c..e611c15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ All notable changes to this project are documented here, following [Keep a Changelog](https://keepachangelog.com/) and semantic versioning. +## [0.1.15] - 2026-09-17 + +### Fixed + +- Surface unreadable files (e.g. EACCES / mode-000) as estimated tokens from their + known size instead of reporting them as clean 0-token sources. + ## [0.1.14] - 2026-09-16 ### Fixed diff --git a/package-lock.json b/package-lock.json index a8bd527..9c33059 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ctxtrim", - "version": "0.1.14", + "version": "0.1.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ctxtrim", - "version": "0.1.14", + "version": "0.1.15", "license": "MIT", "bin": { "ctxtrim": "bin/ctxtrim.js" diff --git a/package.json b/package.json index 634b261..be200eb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ctxtrim", - "version": "0.1.14", + "version": "0.1.15", "description": "Trim what bloats your AI coding context. Scan a repo, find the high-cost/low-value files ballooning your Claude Code / Cursor / Codex context, and write ignore files to cut token cost. Zero dependencies.", "type": "module", "bin": {