# PostToolUse hook for Edit|Write: runs dotnet format on the C# file Claude just edited. # Claude Code sends the tool call as JSON on stdin; tool_input.file_path is absolute. 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("format-cs.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} PostToolUse format-cs.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 } if (-not $call.tool_input.file_path) { exit 0 } $file = [IO.Path]::GetFullPath("$($call.tool_input.file_path)") if (-not $file.EndsWith('.cs', 'OrdinalIgnoreCase') -or -not $file.StartsWith($root + '\', 'OrdinalIgnoreCase')) { Write-HookLog "tool=$($call.tool_name) skipped $($file.Replace($root + '\', ''))" exit 0 } # dotnet format --include wants a path relative to the folder it runs in; an absolute path # matches no file and exits 0 without formatting anything. $relative = $file.Substring($root.Length + 1) $before = (Get-FileHash -LiteralPath $file).Hash $watch = [Diagnostics.Stopwatch]::StartNew() Push-Location $root $output = & dotnet format --include $relative 2>&1 | ForEach-Object { "$_" } $code = $LASTEXITCODE Pop-Location $seconds = [math]::Round($watch.Elapsed.TotalSeconds, 2) $changed = (Get-FileHash -LiteralPath $file).Hash -ne $before Write-HookLog "tool=$($call.tool_name) file=$relative exit=$code changed=$changed dotnet_format_s=$seconds" if ($code -ne 0) { # Any exit code other than 0 or 2 is a non-blocking error: the edit stands, the notice shows stderr. [Console]::Error.WriteLine("dotnet format failed for $relative (exit $code): " + ($output -join ' ')) exit 1 } if ($changed) { [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PostToolUse' additionalContext = "dotnet format reformatted $($relative -replace '\\', '/') after this edit, so the file on disk differs from the text in the edit." } } | ConvertTo-Json -Compress } exit 0