From 2b61276dfd7225d1b814ecf41cb89e54f933b49f Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Thu, 21 May 2026 05:55:28 +0200 Subject: [PATCH] chore(statusline): add PowerShell caveman-statusline hook Windows/cross-platform statusline hook for caveman mode indicator. Reads .caveman-active flag with symlink and size guards to prevent injection via crafted flag files. Co-Authored-By: Claude --- hooks/caveman-statusline.ps1 | 61 ++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 hooks/caveman-statusline.ps1 diff --git a/hooks/caveman-statusline.ps1 b/hooks/caveman-statusline.ps1 new file mode 100644 index 0000000..345749a --- /dev/null +++ b/hooks/caveman-statusline.ps1 @@ -0,0 +1,61 @@ +$ClaudeDir = if ($env:CLAUDE_CONFIG_DIR) { $env:CLAUDE_CONFIG_DIR } else { Join-Path $HOME ".claude" } +$Flag = Join-Path $ClaudeDir ".caveman-active" +if (-not (Test-Path $Flag)) { exit 0 } + +# Refuse reparse points (symlinks / junctions) and oversized files. Without +# this, a local attacker could point the flag at a secret file and have the +# statusline render its bytes (including ANSI escape sequences) to the terminal +# every keystroke. +try { + $Item = Get-Item -LiteralPath $Flag -Force -ErrorAction Stop + if ($Item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { exit 0 } + if ($Item.Length -gt 64) { exit 0 } +} catch { + exit 0 +} + +$Mode = "" +try { + $Raw = Get-Content -LiteralPath $Flag -TotalCount 1 -ErrorAction Stop + if ($null -ne $Raw) { $Mode = ([string]$Raw).Trim() } +} catch { + exit 0 +} + +# Strip anything outside [a-z0-9-] — blocks terminal-escape and OSC hyperlink +# injection via the flag contents. Then whitelist-validate. +$Mode = $Mode.ToLowerInvariant() +$Mode = ($Mode -replace '[^a-z0-9-]', '') + +$Valid = @('off','lite','full','ultra','wenyan-lite','wenyan','wenyan-full','wenyan-ultra','commit','review','compress') +if (-not ($Valid -contains $Mode)) { exit 0 } + +$Esc = [char]27 +if ([string]::IsNullOrEmpty($Mode) -or $Mode -eq "full") { + [Console]::Write("${Esc}[38;5;172m[CAVEMAN]${Esc}[0m") +} else { + $Suffix = $Mode.ToUpperInvariant() + [Console]::Write("${Esc}[38;5;172m[CAVEMAN:$Suffix]${Esc}[0m") +} + +# Savings suffix: on by default. Opt out via CAVEMAN_STATUSLINE_SAVINGS=0. +# Reads a pre-rendered string written by caveman-stats.js. Refuses reparse +# points and strips control bytes (matches statusline.sh hardening). Until +# /caveman-stats has run at least once, the suffix file is absent and nothing +# is rendered — safe default for fresh installs. +if ($env:CAVEMAN_STATUSLINE_SAVINGS -ne "0") { + $SavingsFile = Join-Path $ClaudeDir ".caveman-statusline-suffix" + if (Test-Path $SavingsFile) { + try { + $SavingsItem = Get-Item -LiteralPath $SavingsFile -Force -ErrorAction Stop + if (-not ($SavingsItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -and + $SavingsItem.Length -le 64) { + $Savings = (Get-Content -LiteralPath $SavingsFile -Raw -ErrorAction Stop).TrimEnd() + $Savings = ($Savings -replace '[\x00-\x1F]', '') + if ($Savings.Length -gt 0) { + [Console]::Write(" ${Esc}[38;5;172m$Savings${Esc}[0m") + } + } + } catch {} + } +}