Переглянути джерело

fix(hooks): Strip CRs from installed shell scripts in install.ps1

.gitattributes now pins *.sh to LF at checkout, but a clone predating
it (or a tool that rewrote endings) still carries CRLF, and install.ps1
copied those bytes into ~/.claude/ - breaking the installed
pre-commit-unicode-scan.sh and the repo's own pre-commit gate.

Adds a normalization pass over installed hooks/skills/pigeon/auto-skill
covering *.sh, *.sh.template, and extension-less shebang scripts, plus
an AGENTS.md landmine: extension-less bash entry points must be listed
in .gitattributes explicitly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
0xDarkMatter 1 місяць тому
батько
коміт
f5629052f1
2 змінених файлів з 55 додано та 0 видалено
  1. 7 0
      AGENTS.md
  2. 48 0
      scripts/install.ps1

+ 7 - 0
AGENTS.md

@@ -110,6 +110,13 @@ Requires Sonnet 4+ or Opus 4+.
 - **The landing gate must run the FULL per-skill suite sweep** (`skills/*/tests/run.sh`,
   all of them), never just the touched lanes' — suites assert on shared/sibling
   files, so a change in one skill can silently break another's gate.
+- **Line endings**: `.gitattributes` pins `*.sh` (and, via `* text=auto eol=lf`,
+  every file git detects as text) to LF — bash dies on CRLF (`$'\r': command not
+  found`, shebang exit 127). A checkout that predates the pin still carries CRLF
+  until its files are re-smudged (delete + `git checkout -- .`), and
+  `scripts/install.ps1` strips CRs from installed shell scripts as a backstop.
+  Extension-less shebang scripts ride on git's text-detection heuristic — if one
+  ever reads as binary (e.g. embedded NUL), pin it in `.gitattributes` explicitly.
 - **Executable bit on commit**: scripts under `skills/*/scripts/` and `hooks/*.sh`
   must be tracked `100755`. Git on Windows won't set this for you — `tests/check-exec-bits.sh`
   gates it; a script that "works locally" but fails `bash foo.sh` for another

+ 48 - 0
scripts/install.ps1

@@ -435,6 +435,54 @@ if ((Test-Path $settingsPath) -and (Select-String -Path $settingsPath -Pattern "
 }
 Write-Host ""
 
+# =============================================================================
+# LINE ENDINGS - Strip CRs from every installed shell script
+#
+# bash refuses CRLF: `$'\r': command not found`, and a `#!/bin/bash\r` shebang
+# exits 127 ("No such file or directory"). .gitattributes pins *.sh to LF at
+# checkout, but a clone that predates it (or any tool that rewrote endings)
+# still carries CRLF - on 2026-08-08 that broke the installed
+# ~/.claude/hooks/pre-commit-unicode-scan.sh and with it the repo's own
+# pre-commit gate. Normalizing here makes the installed copy runnable
+# regardless of the source tree's line endings.
+# =============================================================================
+Write-Host "Normalizing shell-script line endings..." -ForegroundColor Cyan
+
+$crFixed = 0
+foreach ($d in @("hooks", "skills", "pigeon", "auto-skill")) {
+    $root = Join-Path $claudeDir $d
+    if (-not (Test-Path $root)) { continue }
+    foreach ($f in (Get-ChildItem -Path $root -Recurse -File)) {
+        $isShell = $f.Name -match '\.sh(\.template)?$'
+        if (-not $isShell -and -not $f.Extension) {
+            # Extension-less files are shell iff they open with a shebang.
+            try {
+                $fs = [System.IO.File]::OpenRead($f.FullName)
+                $buf = New-Object byte[] 2
+                $n = $fs.Read($buf, 0, 2)
+                $fs.Close()
+                $isShell = ($n -eq 2 -and $buf[0] -eq 0x23 -and $buf[1] -eq 0x21)
+            } catch { $isShell = $false }
+        }
+        if (-not $isShell) { continue }
+        try {
+            $raw = [System.IO.File]::ReadAllText($f.FullName)
+            if ($raw.Contains("`r")) {
+                [System.IO.File]::WriteAllText($f.FullName, ($raw -replace "`r", ""))
+                $crFixed++
+            }
+        } catch {
+            Write-Host "  WARNING: could not normalize $($f.FullName) ($($_.Exception.Message))" -ForegroundColor Yellow
+        }
+    }
+}
+if ($crFixed -gt 0) {
+    Write-Host "  Converted $crFixed script(s) from CRLF to LF" -ForegroundColor Green
+} else {
+    Write-Host "  All shell scripts already LF" -ForegroundColor Green
+}
+Write-Host ""
+
 # =============================================================================
 # SUMMARY
 # =============================================================================