# Stop hook: runs dotnet test when Claude finishes. If the tests fail, it blocks the stop and # hands Claude the failures, so the session continues and Claude can fix them. try { $stdin = New-Object System.IO.StreamReader([Console]::OpenStandardInput(), (New-Object System.Text.UTF8Encoding $false)) $raw = $stdin.ReadToEnd() $call = $raw | ConvertFrom-Json -ErrorAction Stop } catch { [Console]::Error.WriteLine("test-on-stop.ps1 could not read the hook input: $($_.Exception.Message)") exit 1 } $root = if ($env:CLAUDE_PROJECT_DIR) { $env:CLAUDE_PROJECT_DIR } else { $call.cwd } $root = [IO.Path]::GetFullPath($root).TrimEnd('\') # Keep what the hook received and a one-line summary (successful hooks show nothing in the transcript). $logDir = Join-Path $root '.claude\hooks\logs' New-Item -ItemType Directory -Force -Path $logDir | Out-Null Add-Content -Path (Join-Path $logDir 'hook-input.jsonl') -Value $raw.Trim() -Encoding UTF8 function Write-HookLog([string]$text) { $since = [math]::Round(((Get-Date) - (Get-Process -Id $PID).StartTime).TotalSeconds, 2) $codePage = try { [Console]::InputEncoding.CodePage } catch { 'none' } $line = '{0} Stop test-on-stop.ps1 {1} {2} cp={3} {4} process_s={5}' -f (Get-Date).ToString('o'), (Get-Process -Id $PID).Path, $PSVersionTable.PSVersion, $codePage, $text, $since Add-Content -Path (Join-Path $logDir 'hooks.log') -Value $line -Encoding UTF8 } # Loop guard from the hooks reference: stop_hook_active is true when Claude is already # continuing because a Stop hook blocked. Let it stop this time instead of blocking again. if ($call.stop_hook_active) { Write-HookLog 'skipped: stop_hook_active=true' exit 0 } $watch = [Diagnostics.Stopwatch]::StartNew() Push-Location $root $output = & dotnet test 2>&1 | ForEach-Object { "$_" } $code = $LASTEXITCODE Pop-Location $seconds = [math]::Round($watch.Elapsed.TotalSeconds, 2) Write-HookLog "dotnet test exit=$code dotnet_test_s=$seconds" if ($code -eq 0) { exit 0 } # Keep the failures, drop restore/build progress, and shorten paths to repository-relative ones. $noise = '^\s*$|Determining projects to restore|All projects are up-to-date|^\s*Restored |\s->\s|^Test run for |^A total of |^\[xUnit\.net' $kept = $output | Where-Object { $_ -notmatch $noise } | ForEach-Object { $_.Replace($root + '\', '') } $text = $kept -join "`n" if ($text.Length -gt 6000) { $text = $text.Substring(0, 6000) + "`n[... output cut at 6000 characters]" } [ordered]@{ decision = 'block' reason = "The Stop hook ran dotnet test from the repository root and it failed (exit code $code):`n$text" } | ConvertTo-Json -Compress exit 0