From a3e316989816a498279624b0ab7272508fbfe459 Mon Sep 17 00:00:00 2001 From: Trong Tran Date: Thu, 2 Jul 2026 10:49:30 +0700 Subject: [PATCH 1/2] fix: rename reserved $input variable in PowerShell hooks Assigning parsed hook JSON to $input (PowerShell's automatic pipeline variable) throws a non-terminating ParameterBindingException whenever the hook receives real piped/redirected stdin - i.e. on every actual Claude Code invocation. The assignment silently fails, $input stays unbound, and the hook exits before reading any input. Renamed to $hookInput in subagent-retro.ps1, prompt-router.ps1, and spec-gate.ps1. Discovered while reproducing the 01-bug-subagent-retro-ps1-matches-clobber fixture; that bug was unreachable until this one was fixed. --- CHANGELOG.md | 5 +++++ hooks/powershell/prompt-router.ps1 | 8 ++++---- hooks/powershell/spec-gate.ps1 | 10 +++++----- hooks/powershell/subagent-retro.ps1 | 8 ++++---- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index adcc97a..3ae6812 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 non-existent `~/.claude/hooks/ck/` directory after the `ck` -> `specwright` rename. - `/sd:setup` Phase 7 (and Phase 1.5) now verify every hook `command` path in `.claude/settings.json` resolves to a file on disk, warning loudly when a hook is not firing. +- `hooks/powershell/subagent-retro.ps1`, `prompt-router.ps1`, and `spec-gate.ps1` no longer assign + parsed hook JSON to `$input` - PowerShell's reserved automatic pipeline variable. Assigning to it + threw a non-terminating `ParameterBindingException` on every real (piped/redirected) stdin + invocation, leaving it unbound and causing every PowerShell hook to exit silently before reading + any input. Renamed to `$hookInput` in all three files. --- diff --git a/hooks/powershell/prompt-router.ps1 b/hooks/powershell/prompt-router.ps1 index 8103e80..de3bf48 100644 --- a/hooks/powershell/prompt-router.ps1 +++ b/hooks/powershell/prompt-router.ps1 @@ -173,11 +173,11 @@ function Get-InProgressSpecs { # ---- main ---- -$input = Read-StdinJson -if ($null -eq $input) { exit 0 } +$hookInput = Read-StdinJson +if ($null -eq $hookInput) { exit 0 } -$prompt = $input.prompt -$cwd = $input.cwd +$prompt = $hookInput.prompt +$cwd = $hookInput.cwd if ([string]::IsNullOrWhiteSpace($prompt) -or [string]::IsNullOrWhiteSpace($cwd)) { exit 0 } if (-not (Test-Path -LiteralPath $cwd)) { exit 0 } diff --git a/hooks/powershell/spec-gate.ps1 b/hooks/powershell/spec-gate.ps1 index 3ba4e72..97fcd41 100644 --- a/hooks/powershell/spec-gate.ps1 +++ b/hooks/powershell/spec-gate.ps1 @@ -166,13 +166,13 @@ function Write-BlockDecision { # ---- main ---- -$input = Read-StdinJson -if ($null -eq $input) { exit 0 } +$hookInput = Read-StdinJson +if ($null -eq $hookInput) { exit 0 } -$toolName = $input.tool_name +$toolName = $hookInput.tool_name if ($toolName -ne 'Edit' -and $toolName -ne 'Write' -and $toolName -ne 'MultiEdit') { exit 0 } -$cwd = $input.cwd +$cwd = $hookInput.cwd if ([string]::IsNullOrWhiteSpace($cwd)) { $cwd = (Get-Location).Path } $config = Get-ProjectConfig -Cwd $cwd @@ -188,7 +188,7 @@ $mode = 'warn' try { if ($config.hooks.specGate.mode) { $mode = [string]$config.hooks.specGate.mode } } catch { } if ($mode -eq 'off') { exit 0 } -$filePath = $input.tool_input.file_path +$filePath = $hookInput.tool_input.file_path if ([string]::IsNullOrWhiteSpace($filePath)) { exit 0 } $rel = ConvertTo-RelativePath -Cwd $cwd -FilePath $filePath diff --git a/hooks/powershell/subagent-retro.ps1 b/hooks/powershell/subagent-retro.ps1 index 229d0ae..c9f9c35 100644 --- a/hooks/powershell/subagent-retro.ps1 +++ b/hooks/powershell/subagent-retro.ps1 @@ -156,14 +156,14 @@ function Remove-StaleStateFiles { # ---- main ---- -$input = Read-StdinJson -if ($null -eq $input) { exit 0 } +$hookInput = Read-StdinJson +if ($null -eq $hookInput) { exit 0 } -$cwd = $input.cwd +$cwd = $hookInput.cwd if ([string]::IsNullOrWhiteSpace($cwd)) { $cwd = (Get-Location).Path } if (-not (Test-Path -LiteralPath $cwd)) { exit 0 } -$sessionId = $input.session_id +$sessionId = $hookInput.session_id if ([string]::IsNullOrWhiteSpace($sessionId)) { $sessionId = 'no-session' } $config = Get-ProjectConfig -Cwd $cwd From 0141463592d45b4d68159ed4c8d6ee8a50709170 Mon Sep 17 00:00:00 2001 From: Trong Tran Date: Thu, 2 Jul 2026 10:51:07 +0700 Subject: [PATCH 2/2] fix: stop $Matches clobber in subagent-retro.ps1 spec-ID extraction Get-IndexSpecs ran the spec-ID regex -match before the in-progress literal -match, so the second match overwrote $Matches and every in-progress spec was reported under the literal ID "in-progress" rather than its real ID. The retro-staleness check then looked for /in-progress/05-retro.md, which never exists, so every in-progress spec was reported stale under the wrong ID. Swap the match order (in-progress first, ID regex last) to match the already-correct sister hooks prompt-router.ps1:167 and spec-gate.ps1:145. Verified against a fixture index (FEAT-123 in-progress, RCA-77 in-progress) - PowerShell now reports FEAT-123 and skips the RCA row, matching hooks/bash/subagent-retro.sh output exactly. --- CHANGELOG.md | 6 ++++++ hooks/powershell/subagent-retro.ps1 | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ae6812..09a478f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 threw a non-terminating `ParameterBindingException` on every real (piped/redirected) stdin invocation, leaving it unbound and causing every PowerShell hook to exit silently before reading any input. Renamed to `$hookInput` in all three files. +- `hooks/powershell/subagent-retro.ps1`: `Get-IndexSpecs` ran the spec-ID regex `-match` before the + `in-progress` literal `-match`, so the second match clobbered `$Matches` and every in-progress + spec was reported under the literal ID `"in-progress"` instead of its real ID (e.g. `FEAT-123`), + causing the retro-staleness check to look at the wrong path and never fire correctly. Swapped + the match order (`in-progress` first, ID regex last) to match the already-correct sister hooks + `prompt-router.ps1` and `spec-gate.ps1`. --- diff --git a/hooks/powershell/subagent-retro.ps1 b/hooks/powershell/subagent-retro.ps1 index c9f9c35..0351b21 100644 --- a/hooks/powershell/subagent-retro.ps1 +++ b/hooks/powershell/subagent-retro.ps1 @@ -72,7 +72,7 @@ function Get-IndexSpecs { return $result } foreach ($line in $lines) { - if ($line -match '(FEAT|BUG|REF|PERF|RCA)-[A-Za-z0-9_\-]+' -and $line -match 'in-progress') { + if ($line -match 'in-progress' -and $line -match '(FEAT|BUG|REF|PERF|RCA)-[A-Za-z0-9_\-]+') { $id = $Matches[0] $type = ($id -split '-')[0] $obj = [pscustomobject]@{