mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-23 02:23:33 +08:00
fix(hooks): rewrite patch_settings_cl_v2_simple.ps1 to avoid argv-dup bug
- 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).
This commit is contained in:
78
docs/fixes/PATCH-SETTINGS-SIMPLE-FIX-20260422.md
Normal file
78
docs/fixes/PATCH-SETTINGS-SIMPLE-FIX-20260422.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# patch_settings_cl_v2_simple.ps1 argv-dup bug workaround (2026-04-22)
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
`docs/fixes/patch_settings_cl_v2_simple.ps1` is the minimal PowerShell
|
||||||
|
helper that patches `~/.claude/settings.local.json` so the observer hook
|
||||||
|
points at `observe-wrapper.sh`. It is the "simple" counterpart of
|
||||||
|
`docs/fixes/install_hook_wrapper.ps1` (PR #1540): it never copies the
|
||||||
|
wrapper script, it only rewrites the settings file.
|
||||||
|
|
||||||
|
The previous version of this helper registered the raw `observe.sh` path
|
||||||
|
as the hook command, shared a single command string across `PreToolUse`
|
||||||
|
and `PostToolUse`, and relied on `ConvertTo-Json` defaults that can emit
|
||||||
|
CRLF line endings. Under Claude Code v2.1.116 the first argv token is
|
||||||
|
duplicated, so the wrapper needs to be invoked with a specific shape and
|
||||||
|
the two hook phases need distinct entries.
|
||||||
|
|
||||||
|
## What the fix does
|
||||||
|
|
||||||
|
- First token is the PATH-resolved `bash` (no quoted `.exe` path), so the
|
||||||
|
argv-dup bug no longer passes a binary as a script. Matches PR #1524 and
|
||||||
|
PR #1540.
|
||||||
|
- The wrapper path is normalized to forward slashes before it is embedded
|
||||||
|
in the hook command, avoiding MSYS backslash handling surprises.
|
||||||
|
- `PreToolUse` and `PostToolUse` receive distinct commands with explicit
|
||||||
|
`pre` / `post` positional arguments.
|
||||||
|
- The settings file is written UTF-8 (no BOM) with CRLF normalized to LF
|
||||||
|
so downstream JSON parsers never see mixed line endings.
|
||||||
|
- Existing hooks (including legacy `observe.sh` entries and unrelated
|
||||||
|
third-party hooks) are preserved — the script only appends the new
|
||||||
|
wrapper entries when they are not already registered.
|
||||||
|
- Idempotent on re-runs: a second invocation recognizes the canonical
|
||||||
|
command strings and logs `[SKIP]` instead of duplicating entries.
|
||||||
|
|
||||||
|
## Resulting command shape
|
||||||
|
|
||||||
|
```
|
||||||
|
bash "C:/Users/<you>/.claude/skills/continuous-learning/hooks/observe-wrapper.sh" pre
|
||||||
|
bash "C:/Users/<you>/.claude/skills/continuous-learning/hooks/observe-wrapper.sh" post
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
pwsh -File docs/fixes/patch_settings_cl_v2_simple.ps1
|
||||||
|
# Windows PowerShell 5.1 is also supported:
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File docs/fixes/patch_settings_cl_v2_simple.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
The script backs up the existing settings file to
|
||||||
|
`settings.local.json.bak-<timestamp>` before writing.
|
||||||
|
|
||||||
|
## PowerShell 5.1 compatibility
|
||||||
|
|
||||||
|
`ConvertFrom-Json -AsHashtable` is PowerShell 7+ only. The script tries
|
||||||
|
`-AsHashtable` first and falls back to a manual `PSCustomObject` →
|
||||||
|
`Hashtable` conversion on Windows PowerShell 5.1. Both hook buckets
|
||||||
|
(`PreToolUse`, `PostToolUse`) and their inner `hooks` arrays are
|
||||||
|
materialized as `System.Collections.ArrayList` before serialization, so
|
||||||
|
PS 5.1's `ConvertTo-Json` cannot collapse single-element arrays into bare
|
||||||
|
objects.
|
||||||
|
|
||||||
|
## Verified cases (dry-run)
|
||||||
|
|
||||||
|
1. Fresh install — no existing settings → creates canonical file.
|
||||||
|
2. Idempotent re-run — existing canonical file → `[SKIP]` both phases,
|
||||||
|
file contents unchanged apart from the pre-write backup.
|
||||||
|
3. Legacy `observe.sh` present → preserves the legacy entries and
|
||||||
|
appends the new `observe-wrapper.sh` entries alongside them.
|
||||||
|
|
||||||
|
All three cases produce LF-only output and match the shape registered by
|
||||||
|
PR #1524's manual fix to `settings.local.json`.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- PR #1524 — settings.local.json shape fix (same argv-dup root cause)
|
||||||
|
- PR #1539 — locale-independent `detect-project.sh`
|
||||||
|
- PR #1540 — `install_hook_wrapper.ps1` argv-dup fix (companion script)
|
||||||
187
docs/fixes/patch_settings_cl_v2_simple.ps1
Normal file
187
docs/fixes/patch_settings_cl_v2_simple.ps1
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
# 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
|
||||||
Reference in New Issue
Block a user