fix(hooks): rewrite install_hook_wrapper.ps1 to avoid argv-dup bug

Under Claude Code v2.1.116 the first argv token of a hook command is
duplicated. When the token is a quoted Windows .exe path, bash.exe is
re-invoked with itself as script (exit 126). PR #1524 fixed the shape
of settings.local.json; this script keeps the installer consistent so
re-running it does not regenerate the broken form.

Changes:
- First token is now PATH-resolved `bash` instead of the quoted bash.exe
- Wrapper path is normalized to forward slashes for MSYS safety
- PreToolUse and PostToolUse get distinct pre/post positional arguments
- JSON output is written with LF endings (no mixed CRLF/LF)

Companion doc: docs/fixes/INSTALL-HOOK-WRAPPER-FIX-20260422.md
This commit is contained in:
suusuu0927 2026-04-22 06:19:15 +09:00
parent d87304573c
commit c32f0fffb1
2 changed files with 126 additions and 0 deletions

View File

@ -0,0 +1,53 @@
# 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.
## 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`

View File

@ -0,0 +1,73 @@
# 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.
$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
}
# 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
$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 = Get-Content -Raw -Path $SettingsPath -Encoding UTF8 | ConvertFrom-Json -AsHashtable
# 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'
foreach ($entry in $settings.hooks.PreToolUse) {
foreach ($h in $entry.hooks) {
$h.command = $preCmd
}
}
foreach ($entry in $settings.hooks.PostToolUse) {
foreach ($h in $entry.hooks) {
$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"