From 1c54e7fae0b40ab376291f445a635d21d60aa524 Mon Sep 17 00:00:00 2001 From: James Maes Date: Sun, 8 Nov 2026 08:01:58 -0600 Subject: [PATCH] fix(core): DPAPI keyring works when started from PowerShell 7 Started from pwsh 7, the child Windows PowerShell 5.1 inherits PowerShell 7's PSModulePath and cannot load Microsoft.PowerShell.Security, so ConvertTo-SecureString fails and every DPAPI set/get reports "Windows credential protection failed". The release workflow's Windows lane (steps in pwsh) hit it in the existing DPAPI test. The keyring now spawns PowerShell without PSModulePath; each shell computes its own. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 7 +++++++ packages/core/src/session/keyring.ts | 5 ++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db237ac..319c1f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -102,6 +102,13 @@ changes listed under Unreleased above as of this tag. the Homebrew tap and Scoop bucket. The `cli-v*` tags and GitHub releases are retired; `scripts/install.sh`, the formula and the Scoop manifest download from `https://git.kof22.com/QRun-IO/qqq-cli/releases`. +### Fixed + +- **Windows: the DPAPI keyring works when `qqq` is started from PowerShell 7.** The inherited `PSModulePath` + named PowerShell 7's modules, so Windows PowerShell 5.1 could not load `Microsoft.PowerShell.Security` and every + sign-in failed with "Windows credential protection failed". The keyring now starts PowerShell without it. + Found by the first native Windows release lane. + ## [1.0.0] - 2026-11-01 `qqq` 1.0: the command-line client and the full-screen terminal UI (`qqq ui`), shipped together as one diff --git a/packages/core/src/session/keyring.ts b/packages/core/src/session/keyring.ts index 25e4f80..8197e57 100644 --- a/packages/core/src/session/keyring.ts +++ b/packages/core/src/session/keyring.ts @@ -110,7 +110,10 @@ export class DpapiFileStore implements SecretStore { + '[Console]::Write((ConvertTo-Json -InputObject $qqqResult -Compress))' return new Promise((resolve, reject) => { const failed = () => reject(new Error('Windows credential protection failed; check PowerShell and your Windows profile')) - const child = execFile(shell, ['-NoProfile', '-NonInteractive', '-Command', command], { timeout: 15000 }, (error, stdout) => { + // Started from PowerShell 7, the inherited PSModulePath names PowerShell 7's modules, and Windows PowerShell 5.1 + // then cannot load Microsoft.PowerShell.Security (ConvertTo-SecureString). Without it, each shell uses its own. + const env = Object.fromEntries(Object.entries(process.env).filter(([name]) => name.toUpperCase() !== 'PSMODULEPATH')) + const child = execFile(shell, ['-NoProfile', '-NonInteractive', '-Command', command], { timeout: 15000, env }, (error, stdout) => { if (error) { failed(); return } try { const result: unknown = JSON.parse(stdout.trim()) -- 2.54.0