mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-23 04:26:54 +08:00
Merge pull request #1540 from suusuu0927/claude/install-hook-wrapper-argv-dup-fix-20260422
fix(hooks): rewrite install_hook_wrapper.ps1 to avoid argv-dup bug
This commit is contained in:
commit
8ae1499122
66
docs/fixes/INSTALL-HOOK-WRAPPER-FIX-20260422.md
Normal file
66
docs/fixes/INSTALL-HOOK-WRAPPER-FIX-20260422.md
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
# install_hook_wrapper.ps1 argv-dup bug workaround (2026-04-22)
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
`docs/fixes/install_hook_wrapper.ps1` is the PowerShell helper that copies
|
||||||
|
`observe-wrapper.sh` into `~/.claude/skills/continuous-learning/hooks/` and
|
||||||
|
rewrites `~/.claude/settings.local.json` so the observer hook points at it.
|
||||||
|
|
||||||
|
The previous version produced a hook command of the form:
|
||||||
|
|
||||||
|
```
|
||||||
|
"C:\Program Files\Git\bin\bash.exe" "C:\Users\...\observe-wrapper.sh"
|
||||||
|
```
|
||||||
|
|
||||||
|
Under Claude Code v2.1.116 the first argv token is duplicated. When that token
|
||||||
|
is a quoted Windows executable path, `bash.exe` is re-invoked with itself as
|
||||||
|
its `$0`, which fails with `cannot execute binary file` (exit 126). PR #1524
|
||||||
|
documents the root cause; this script is a companion that keeps the installer
|
||||||
|
in sync with the fixed `settings.local.json` layout.
|
||||||
|
|
||||||
|
## What the fix does
|
||||||
|
|
||||||
|
- First token is now the PATH-resolved `bash` (no quoted `.exe` path), so the
|
||||||
|
argv-dup bug no longer passes a binary as a script.
|
||||||
|
- 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, matching the shape the wrapper expects.
|
||||||
|
- The settings file is written with LF line endings so downstream JSON parsers
|
||||||
|
never see mixed CRLF/LF output from `ConvertTo-Json`.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
# Place observe-wrapper.sh next to this script, then:
|
||||||
|
pwsh -File docs/fixes/install_hook_wrapper.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
The script backs up `settings.local.json` 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 by running `powershell -NoProfile -File
|
||||||
|
docs/fixes/install_hook_wrapper.ps1` on a Windows 11 machine with only
|
||||||
|
Windows PowerShell 5.1 installed (no `pwsh`).
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- PR #1524 — settings.local.json shape fix (same argv-dup root cause)
|
||||||
|
- PR #1511 — skip `AppInstallerPythonRedirector.exe` in observer python resolution
|
||||||
|
- PR #1539 — locale-independent `detect-project.sh`
|
||||||
|
- PR #1542 — `patch_settings_cl_v2_simple.ps1` companion fix
|
||||||
167
docs/fixes/install_hook_wrapper.ps1
Normal file
167
docs/fixes/install_hook_wrapper.ps1
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
# Install observe-wrapper.sh + rewrite settings.local.json to use it
|
||||||
|
# No Japanese literals - uses $PSScriptRoot instead
|
||||||
|
# argv-dup bug workaround: use `bash` (PATH-resolved) as first token and
|
||||||
|
# normalize wrapper path to forward slashes. See PR #1524.
|
||||||
|
#
|
||||||
|
# PowerShell 5.1 compatibility:
|
||||||
|
# - `ConvertFrom-Json -AsHashtable` is PS 7+ only; fall back to a manual
|
||||||
|
# PSCustomObject -> Hashtable conversion on Windows PowerShell 5.1.
|
||||||
|
# - PS 5.1 `ConvertTo-Json` collapses single-element arrays inside
|
||||||
|
# Hashtables into bare objects. Normalize the hook buckets
|
||||||
|
# (PreToolUse / PostToolUse) and their inner `hooks` arrays as
|
||||||
|
# `System.Collections.ArrayList` before serialization to preserve
|
||||||
|
# array shape.
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$SkillHooks = "$env:USERPROFILE\.claude\skills\continuous-learning\hooks"
|
||||||
|
$WrapperSrc = Join-Path $PSScriptRoot "observe-wrapper.sh"
|
||||||
|
$WrapperDst = "$SkillHooks\observe-wrapper.sh"
|
||||||
|
$SettingsPath = "$env:USERPROFILE\.claude\settings.local.json"
|
||||||
|
# Use PATH-resolved `bash` to avoid Claude Code v2.1.116 argv-dup bug that
|
||||||
|
# double-passes the first token when the quoted path is a Windows .exe.
|
||||||
|
$BashExe = "bash"
|
||||||
|
|
||||||
|
Write-Host "=== Install Hook Wrapper ===" -ForegroundColor Cyan
|
||||||
|
Write-Host "ScriptRoot: $PSScriptRoot"
|
||||||
|
Write-Host "WrapperSrc: $WrapperSrc"
|
||||||
|
|
||||||
|
if (-not (Test-Path $WrapperSrc)) {
|
||||||
|
Write-Host "[ERROR] Source not found: $WrapperSrc" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensure the hook destination directory exists (fresh installs have no
|
||||||
|
# skills/continuous-learning/hooks tree yet).
|
||||||
|
$dstDir = Split-Path $WrapperDst
|
||||||
|
if (-not (Test-Path $dstDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $dstDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Helpers ------------------------------------------------------------
|
||||||
|
|
||||||
|
# Convert a PSCustomObject tree (as returned by ConvertFrom-Json on PS 5.1)
|
||||||
|
# into nested Hashtables/ArrayLists so the merge logic below works uniformly
|
||||||
|
# and so ConvertTo-Json preserves single-element arrays on PS 5.1.
|
||||||
|
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]) {
|
||||||
|
$list = [System.Collections.ArrayList]::new()
|
||||||
|
foreach ($item in $InputObject) {
|
||||||
|
$null = $list.Add((ConvertTo-HashtableRecursive -InputObject $item))
|
||||||
|
}
|
||||||
|
return ,$list
|
||||||
|
}
|
||||||
|
return $InputObject
|
||||||
|
}
|
||||||
|
|
||||||
|
function Read-SettingsAsHashtable {
|
||||||
|
param([string]$Path)
|
||||||
|
$raw = Get-Content -Raw -Path $Path -Encoding UTF8
|
||||||
|
if ([string]::IsNullOrWhiteSpace($raw)) { return @{} }
|
||||||
|
try {
|
||||||
|
return ($raw | ConvertFrom-Json -AsHashtable)
|
||||||
|
} catch {
|
||||||
|
$obj = $raw | ConvertFrom-Json
|
||||||
|
return (ConvertTo-HashtableRecursive -InputObject $obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-ArrayList {
|
||||||
|
param($Value)
|
||||||
|
$list = [System.Collections.ArrayList]::new()
|
||||||
|
foreach ($item in @($Value)) { $null = $list.Add($item) }
|
||||||
|
return ,$list
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1) Copy wrapper + LF normalization ---------------------------------
|
||||||
|
Write-Host "[1/4] Copy wrapper to $WrapperDst" -ForegroundColor Yellow
|
||||||
|
$content = Get-Content -Raw -Path $WrapperSrc
|
||||||
|
$contentLf = $content -replace "`r`n","`n"
|
||||||
|
$utf8 = [System.Text.UTF8Encoding]::new($false)
|
||||||
|
[System.IO.File]::WriteAllText($WrapperDst, $contentLf, $utf8)
|
||||||
|
Write-Host " [OK] wrapper installed with LF endings" -ForegroundColor Green
|
||||||
|
|
||||||
|
# --- 2) Backup settings -------------------------------------------------
|
||||||
|
Write-Host "[2/4] Backup settings.local.json" -ForegroundColor Yellow
|
||||||
|
if (-not (Test-Path $SettingsPath)) {
|
||||||
|
Write-Host "[ERROR] Settings file not found: $SettingsPath" -ForegroundColor Red
|
||||||
|
Write-Host " Run patch_settings_cl_v2_simple.ps1 first to bootstrap the file." -ForegroundColor Yellow
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$backup = "$SettingsPath.bak-$(Get-Date -Format 'yyyyMMdd-HHmmss')"
|
||||||
|
Copy-Item $SettingsPath $backup -Force
|
||||||
|
Write-Host " [OK] $backup" -ForegroundColor Green
|
||||||
|
|
||||||
|
# --- 3) Rewrite command path in settings.local.json ---------------------
|
||||||
|
Write-Host "[3/4] Rewrite hook command to wrapper" -ForegroundColor Yellow
|
||||||
|
$settings = Read-SettingsAsHashtable -Path $SettingsPath
|
||||||
|
|
||||||
|
# Normalize wrapper path to forward slashes so bash (MSYS/Git Bash) does not
|
||||||
|
# mangle backslashes; quoting keeps spaces safe.
|
||||||
|
$wrapperPath = $WrapperDst -replace '\\','/'
|
||||||
|
$preCmd = $BashExe + ' "' + $wrapperPath + '" pre'
|
||||||
|
$postCmd = $BashExe + ' "' + $wrapperPath + '" post'
|
||||||
|
|
||||||
|
if (-not $settings.ContainsKey("hooks") -or $null -eq $settings["hooks"]) {
|
||||||
|
$settings["hooks"] = @{}
|
||||||
|
}
|
||||||
|
foreach ($key in @("PreToolUse", "PostToolUse")) {
|
||||||
|
if (-not $settings.hooks.ContainsKey($key) -or $null -eq $settings.hooks[$key]) {
|
||||||
|
$settings.hooks[$key] = [System.Collections.ArrayList]::new()
|
||||||
|
} elseif (-not ($settings.hooks[$key] -is [System.Collections.ArrayList])) {
|
||||||
|
$settings.hooks[$key] = (ConvertTo-ArrayList -Value $settings.hooks[$key])
|
||||||
|
}
|
||||||
|
# Inner `hooks` arrays need the same ArrayList normalization to
|
||||||
|
# survive PS 5.1 ConvertTo-Json serialization.
|
||||||
|
foreach ($entry in $settings.hooks[$key]) {
|
||||||
|
if ($entry -is [System.Collections.IDictionary] -and $entry.ContainsKey("hooks") -and
|
||||||
|
-not ($entry["hooks"] -is [System.Collections.ArrayList])) {
|
||||||
|
$entry["hooks"] = (ConvertTo-ArrayList -Value $entry["hooks"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Point every existing hook command at the wrapper with the appropriate
|
||||||
|
# positional argument. The entry shape is preserved exactly; only the
|
||||||
|
# `command` field is rewritten.
|
||||||
|
foreach ($entry in $settings.hooks.PreToolUse) {
|
||||||
|
foreach ($h in @($entry.hooks)) {
|
||||||
|
if ($h -is [System.Collections.IDictionary]) { $h["command"] = $preCmd }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($entry in $settings.hooks.PostToolUse) {
|
||||||
|
foreach ($h in @($entry.hooks)) {
|
||||||
|
if ($h -is [System.Collections.IDictionary]) { $h["command"] = $postCmd }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$json = $settings | ConvertTo-Json -Depth 20
|
||||||
|
# Normalize CRLF -> LF so hook parsers never see mixed line endings.
|
||||||
|
$jsonLf = $json -replace "`r`n","`n"
|
||||||
|
[System.IO.File]::WriteAllText($SettingsPath, $jsonLf, $utf8)
|
||||||
|
Write-Host " [OK] command updated" -ForegroundColor Green
|
||||||
|
Write-Host " PreToolUse command: $preCmd"
|
||||||
|
Write-Host " PostToolUse command: $postCmd"
|
||||||
|
|
||||||
|
# --- 4) Verify ----------------------------------------------------------
|
||||||
|
Write-Host "[4/4] Verify" -ForegroundColor Yellow
|
||||||
|
Get-Content $SettingsPath | Select-String "command"
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "=== DONE ===" -ForegroundColor Green
|
||||||
|
Write-Host "Next: Launch Claude CLI and run any command to trigger observations.jsonl"
|
||||||
Loading…
x
Reference in New Issue
Block a user