diff --git a/src/lib/credentials.test.ts b/src/lib/credentials.test.ts index 447d9d3..ad93031 100644 --- a/src/lib/credentials.test.ts +++ b/src/lib/credentials.test.ts @@ -216,15 +216,21 @@ describe('ensureRestrictiveMode', () => { spawnSync: spawn, }); - expect(spawn).toHaveBeenCalledWith( - 'icacls', - [credentialsPath, '/inheritance:r', '/grant:r', 'alice:F'], - { - shell: false, - stdio: 'ignore', - windowsHide: true, - }, - ); + // 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, '/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', () => { 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,