mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-23 04:26:54 +08:00
- Use PATH-resolved `bash` as first token instead of quoted `.exe` path so Claude Code v2.1.116 argv duplication does not feed a binary to bash as its $0 (repro: exit 126 "cannot execute binary file"). - Point the command at `observe-wrapper.sh` and pass distinct `pre` / `post` positional arguments so PreToolUse and PostToolUse are registered as separate entries. - Normalize the wrapper path to forward slashes before embedding in the hook command to avoid MSYS backslash surprises. - Write UTF-8 (no BOM) with CRLF normalized to LF so downstream JSON parsers never see mixed line endings. - Preserve existing hooks (legacy `observe.sh`, third-party entries) by appending only when the canonical command string is not already registered. Re-runs are idempotent ([SKIP] both phases). - Keep the script compatible with Windows PowerShell 5.1: fall back to a manual PSCustomObject → Hashtable conversion when `ConvertFrom-Json -AsHashtable` is unavailable, and materialize hook arrays as `System.Collections.ArrayList` so single-element arrays survive PS 5.1 `ConvertTo-Json` serialization. Companion to PR #1524 (settings.local.json shape fix) and PR #1540 (install_hook_wrapper.ps1 argv-dup fix).
188 lines
7.3 KiB
PowerShell
188 lines
7.3 KiB
PowerShell
# Simple patcher for settings.local.json - CL v2 hooks (argv-dup safe)
|
|
#
|
|
# No Japanese literals - keeps the file ASCII-only so PowerShell parses it
|
|
# regardless of the active code page.
|
|
#
|
|
# argv-dup bug workaround (Claude Code v2.1.116):
|
|
# - Use PATH-resolved `bash` (no quoted .exe) as the first argv token.
|
|
# - Point the hook at observe-wrapper.sh (not observe.sh).
|
|
# - Pass `pre` / `post` as explicit positional arguments so PreToolUse and
|
|
# PostToolUse are registered as distinct commands.
|
|
# - Normalize the wrapper path to forward slashes to keep MSYS/Git Bash
|
|
# happy and write the JSON with LF endings only.
|
|
#
|
|
# References:
|
|
# - PR #1524 (settings.local.json argv-dup fix)
|
|
# - PR #1540 (install_hook_wrapper.ps1 argv-dup fix)
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
$SettingsPath = "$env:USERPROFILE\.claude\settings.local.json"
|
|
$WrapperDst = "$env:USERPROFILE\.claude\skills\continuous-learning\hooks\observe-wrapper.sh"
|
|
$BashExe = "bash"
|
|
|
|
# Normalize wrapper path to forward slashes and build distinct pre/post
|
|
# commands. Quoting keeps spaces in the path safe.
|
|
$wrapperPath = $WrapperDst -replace '\\','/'
|
|
$preCmd = $BashExe + ' "' + $wrapperPath + '" pre'
|
|
$postCmd = $BashExe + ' "' + $wrapperPath + '" post'
|
|
|
|
Write-Host "=== CL v2 Simple Patcher (argv-dup safe) ===" -ForegroundColor Cyan
|
|
Write-Host "Target : $SettingsPath"
|
|
Write-Host "Wrapper : $wrapperPath"
|
|
Write-Host "Pre command : $preCmd"
|
|
Write-Host "Post command: $postCmd"
|
|
|
|
# Ensure parent dir exists
|
|
$parent = Split-Path $SettingsPath
|
|
if (-not (Test-Path $parent)) {
|
|
New-Item -ItemType Directory -Path $parent -Force | Out-Null
|
|
}
|
|
|
|
function New-HookEntry {
|
|
param([string]$Command)
|
|
# Inner `hooks` uses ArrayList so a single-element list does not get
|
|
# collapsed into an object when PS 5.1 ConvertTo-Json serializes the
|
|
# enclosing Hashtable.
|
|
$inner = [System.Collections.ArrayList]::new()
|
|
$null = $inner.Add(@{ type = "command"; command = $Command })
|
|
return @{
|
|
matcher = "*"
|
|
hooks = $inner
|
|
}
|
|
}
|
|
|
|
# Convert a PSCustomObject tree (as returned by ConvertFrom-Json on PS 5.1)
|
|
# into nested Hashtables/Arrays so the merge logic below works uniformly.
|
|
# PS 7+ gets the same shape via `ConvertFrom-Json -AsHashtable` directly.
|
|
function ConvertTo-HashtableRecursive {
|
|
param($InputObject)
|
|
if ($null -eq $InputObject) { return $null }
|
|
if ($InputObject -is [System.Collections.IDictionary]) {
|
|
$result = @{}
|
|
foreach ($key in $InputObject.Keys) {
|
|
$result[$key] = ConvertTo-HashtableRecursive -InputObject $InputObject[$key]
|
|
}
|
|
return $result
|
|
}
|
|
if ($InputObject -is [System.Management.Automation.PSCustomObject]) {
|
|
$result = @{}
|
|
foreach ($prop in $InputObject.PSObject.Properties) {
|
|
$result[$prop.Name] = ConvertTo-HashtableRecursive -InputObject $prop.Value
|
|
}
|
|
return $result
|
|
}
|
|
if ($InputObject -is [System.Collections.IList] -or $InputObject -is [System.Array]) {
|
|
# Use ArrayList so PS 5.1 ConvertTo-Json preserves single-element
|
|
# arrays instead of collapsing them into objects. Plain Object[]
|
|
# suffers from that collapse when embedded in a Hashtable value.
|
|
$result = [System.Collections.ArrayList]::new()
|
|
foreach ($item in $InputObject) {
|
|
$null = $result.Add((ConvertTo-HashtableRecursive -InputObject $item))
|
|
}
|
|
return ,$result
|
|
}
|
|
return $InputObject
|
|
}
|
|
|
|
function Read-SettingsAsHashtable {
|
|
param([string]$Path)
|
|
$raw = Get-Content -Raw -Path $Path -Encoding UTF8
|
|
if ([string]::IsNullOrWhiteSpace($raw)) { return @{} }
|
|
# Prefer `-AsHashtable` (PS 7+); fall back to manual conversion on PS 5.1
|
|
# where that parameter does not exist.
|
|
try {
|
|
return ($raw | ConvertFrom-Json -AsHashtable)
|
|
} catch {
|
|
$obj = $raw | ConvertFrom-Json
|
|
return (ConvertTo-HashtableRecursive -InputObject $obj)
|
|
}
|
|
}
|
|
|
|
$preEntry = New-HookEntry -Command $preCmd
|
|
$postEntry = New-HookEntry -Command $postCmd
|
|
|
|
if (Test-Path $SettingsPath) {
|
|
$backup = "$SettingsPath.bak-$(Get-Date -Format 'yyyyMMdd-HHmmss')"
|
|
Copy-Item $SettingsPath $backup -Force
|
|
Write-Host "[BACKUP] $backup" -ForegroundColor Yellow
|
|
|
|
try {
|
|
$existing = Read-SettingsAsHashtable -Path $SettingsPath
|
|
} catch {
|
|
Write-Host "[WARN] Failed to parse existing JSON, will overwrite (backup preserved)" -ForegroundColor Yellow
|
|
$existing = @{}
|
|
}
|
|
if ($null -eq $existing) { $existing = @{} }
|
|
|
|
if (-not $existing.ContainsKey("hooks")) {
|
|
$existing["hooks"] = @{}
|
|
}
|
|
# Normalize the two hook buckets into ArrayList so both existing and newly
|
|
# added entries survive PS 5.1 ConvertTo-Json array collapsing.
|
|
foreach ($key in @("PreToolUse", "PostToolUse")) {
|
|
if (-not $existing.hooks.ContainsKey($key)) {
|
|
$existing.hooks[$key] = [System.Collections.ArrayList]::new()
|
|
} elseif (-not ($existing.hooks[$key] -is [System.Collections.ArrayList])) {
|
|
$list = [System.Collections.ArrayList]::new()
|
|
foreach ($item in @($existing.hooks[$key])) { $null = $list.Add($item) }
|
|
$existing.hooks[$key] = $list
|
|
}
|
|
# Each entry's inner `hooks` array needs the same treatment so legacy
|
|
# single-element arrays do not serialize as bare objects.
|
|
foreach ($entry in $existing.hooks[$key]) {
|
|
if ($entry -is [System.Collections.IDictionary] -and $entry.ContainsKey("hooks") -and
|
|
-not ($entry["hooks"] -is [System.Collections.ArrayList])) {
|
|
$innerList = [System.Collections.ArrayList]::new()
|
|
foreach ($item in @($entry["hooks"])) { $null = $innerList.Add($item) }
|
|
$entry["hooks"] = $innerList
|
|
}
|
|
}
|
|
}
|
|
|
|
# Duplicate check uses the exact command string so legacy observe.sh
|
|
# entries are left in place unless re-run manually removes them.
|
|
$hasPre = $false
|
|
foreach ($e in $existing.hooks.PreToolUse) {
|
|
foreach ($h in @($e.hooks)) { if ($h.command -eq $preCmd) { $hasPre = $true } }
|
|
}
|
|
$hasPost = $false
|
|
foreach ($e in $existing.hooks.PostToolUse) {
|
|
foreach ($h in @($e.hooks)) { if ($h.command -eq $postCmd) { $hasPost = $true } }
|
|
}
|
|
|
|
if (-not $hasPre) {
|
|
$null = $existing.hooks.PreToolUse.Add($preEntry)
|
|
Write-Host "[ADD] PreToolUse" -ForegroundColor Green
|
|
} else {
|
|
Write-Host "[SKIP] PreToolUse already registered" -ForegroundColor Gray
|
|
}
|
|
if (-not $hasPost) {
|
|
$null = $existing.hooks.PostToolUse.Add($postEntry)
|
|
Write-Host "[ADD] PostToolUse" -ForegroundColor Green
|
|
} else {
|
|
Write-Host "[SKIP] PostToolUse already registered" -ForegroundColor Gray
|
|
}
|
|
|
|
$json = $existing | ConvertTo-Json -Depth 20
|
|
} else {
|
|
Write-Host "[CREATE] new settings.local.json" -ForegroundColor Green
|
|
$newSettings = @{
|
|
hooks = @{
|
|
PreToolUse = @($preEntry)
|
|
PostToolUse = @($postEntry)
|
|
}
|
|
}
|
|
$json = $newSettings | ConvertTo-Json -Depth 20
|
|
}
|
|
|
|
# Write UTF-8 no BOM and normalize CRLF -> LF so hook parsers never see
|
|
# mixed line endings.
|
|
$jsonLf = $json -replace "`r`n","`n"
|
|
$utf8 = [System.Text.UTF8Encoding]::new($false)
|
|
[System.IO.File]::WriteAllText($SettingsPath, $jsonLf, $utf8)
|
|
|
|
Write-Host ""
|
|
Write-Host "=== Patch SUCCESS ===" -ForegroundColor Green
|
|
Write-Host ""
|
|
Get-Content -Path $SettingsPath -Encoding UTF8
|