From 0488f22f7df667407ccdf1b4e966ab4a0b5c24af Mon Sep 17 00:00:00 2001 From: "yaxin.liu" Date: Mon, 3 Aug 2026 17:22:51 +0800 Subject: [PATCH 1/2] fix(credentials): restore Windows ACL inheritance to fix EPERM on read The previous icacls call used /inheritance:r which strips all inherited ACEs and relies solely on the USERNAME env grant. On Windows, USERNAME may not resolve to the same SID that owns the file (e.g. Microsoft Account or domain account mismatches), leaving the file unreadable by anyone including the file owner. Fix: call icacls /reset first to re-enable inherited permissions from the parent directory, then add an explicit /grant:r entry as belt-and-suspenders full-control grant. Verified on Windows 11 Pro: testsprite doctor passes after fix. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/lib/credentials.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/lib/credentials.ts b/src/lib/credentials.ts index c67944b..efe8525 100644 --- a/src/lib/credentials.ts +++ b/src/lib/credentials.ts @@ -231,7 +231,21 @@ function ensureWindowsRestrictiveAcl(path: string, options: RestrictiveModeOptio } const run = options.spawnSync ?? spawnSync; - const result = run('icacls', [path, '/inheritance:r', '/grant:r', `${username}:F`], { + + // Reset to re-enable inheritance from the parent directory first. + // Using /inheritance:r (the previous approach) strips all inherited ACEs and + // relies solely on the USERNAME-based grant — on Windows the env USERNAME may + // not resolve to the same SID that owns the file (e.g. Microsoft Account / + // domain account mismatches), which leaves the file unreadable by anyone. + // /reset restores inherited ACEs so the owner can always access the file, then + // the explicit /grant:r adds a belt-and-suspenders Full-Control entry. + run('icacls', [path, '/reset'], { + shell: false, + stdio: 'ignore', + windowsHide: true, + }); + + const result = run('icacls', [path, '/grant:r', `${username}:F`], { shell: false, stdio: 'ignore', windowsHide: true, From 4010631f21300ae19c77e77268ce0edc9c2b8eb5 Mon Sep 17 00:00:00 2001 From: "yaxin.liu" Date: Mon, 3 Aug 2026 17:42:49 +0800 Subject: [PATCH 2/2] test(credentials): update Windows ACL test to expect /reset + /grant:r calls --- src/lib/credentials.test.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/lib/credentials.test.ts b/src/lib/credentials.test.ts index 447d9d3..485132f 100644 --- a/src/lib/credentials.test.ts +++ b/src/lib/credentials.test.ts @@ -216,15 +216,31 @@ describe('ensureRestrictiveMode', () => { spawnSync: spawn, }); - expect(spawn).toHaveBeenCalledWith( + // First call: /reset re-enables inheritance from the parent directory so the + // owner can always access the file (fixes EPERM on Microsoft Account / domain + // account machines where USERNAME does not resolve to the file-owner SID). + expect(spawn).toHaveBeenNthCalledWith( + 1, 'icacls', - [credentialsPath, '/inheritance:r', '/grant:r', 'alice:F'], + [credentialsPath, '/reset'], { shell: false, stdio: 'ignore', windowsHide: true, }, ); + // Second call: /grant:r adds an explicit Full Control entry as belt-and-suspenders. + expect(spawn).toHaveBeenNthCalledWith( + 2, + 'icacls', + [credentialsPath, '/grant:r', 'alice:F'], + { + shell: false, + stdio: 'ignore', + windowsHide: true, + }, + ); + expect(spawn).toHaveBeenCalledTimes(2); }); it('warns on Windows when credentials ACL tightening cannot run', () => {