feat(hooks): PowerShell hook variants behind --hook-shell powershell - #97
Conversation
|
VERDICT: PASS Clean, self-contained addition. All blocking criteria checked and clear:
Implementation details verified: |
6849cb2 to
754297f
Compare
|
VERDICT: COMMENT-ONLY Advisoryconfigure.py:2118 -- The dict maps the literal bash command string to its PowerShell equivalent. If the marker-clear command ever changes (e.g., a fourth marker file added alongside The fix is low-ceremony: extract the bash command into a named constant that both Everything else checks out: CHANGELOG entry present and detailed; no third-party code (NOTICE unchanged, correctly); |
93aaa46 to
e3c6009
Compare
754297f to
5d79acd
Compare
|
VERDICT: PASS All six ps1 hooks are ASCII and BOM-free (enforced by --check), exit codes match their bash siblings (block-dangerous-bash and scan-secrets both use exit 2; microbit-enforcer uses exit 1, consistent with the bash version's own exit codes), target_path_for in config_schema.py:1109-1112 correctly routes microbit-enforcer.ps1 to .claude/hooks/ so powershell_hook_command's hardcoded path is accurate for all six hooks, and the POWERSHELL_INLINE_COMMANDS dict key matches the settings-patch inline command string exactly. CHANGELOG ## Unreleased entry present; no third-party code; templates/discipline-skills/ untouched; no new settings.json schema keys; single logical change. |
e3c6009 to
23f2180
Compare
5d79acd to
e6b9724
Compare
|
VERDICT: PASS Clean, focused split from #95. Nothing blocking. The per-entry swap logic in |
23f2180 to
dfe8323
Compare
The `shell: "bash"` fix covers Windows *with* Git Bash. This covers the
machines without it, where "bash" has nothing to resolve to.
Six hooks now ship a .ps1 sibling -- block-dangerous-bash, scan-secrets,
format-on-write, stop-run-checks, pre-compact-snapshot, microbit-enforcer --
and --hook-shell powershell swaps a .sh for its sibling by name, per entry,
setting "shell": "powershell" on just those hooks. Everything without a
sibling stays bash, which is correct rather than lazy: check-package-
availability probes apt/brew and sessionstart-drift-check is jq-driven, so
both are Linux/macOS-shaped by nature. The answer persists to
.claude-config.json like the rest of the intake; the default is unchanged.
Three Windows-specific traps, each found by running the hooks rather than by
reading about them:
a) Windows PowerShell 5.1 -- still the default -- reads .ps1 as the system
ANSI code page unless the file carries a BOM. A UTF-8 em-dash decodes to
a cp1252 smart quote, which PowerShell accepts as a string delimiter: the
string terminated mid-line and microbit-enforcer.ps1 failed to parse. All
shipped .ps1 are ASCII and BOM-free, and --check enforces that, plus the
.sh pairing -- an orphan .ps1 would never be installed, since the swap is
by name.
b) Windows ships execution policy Restricted, so naming a .ps1 directly
fails with "running scripts is disabled on this system" -- a silent,
machine-dependent break of exactly the kind this work exists to prevent.
The generated command spawns PowerShell with -ExecutionPolicy Bypass,
which applies only to that child process running a script the user
installed deliberately, and never changes machine policy.
c) A wrapping `powershell -Command` collapses any non-zero child exit to 1,
which would have turned a PreToolUse BLOCK (exit 2) into a mere
non-blocking error -- the safety hooks would have appeared to work while
silently permitting everything. The command ends with
`; exit $LASTEXITCODE`, and exit 2 was then verified to survive both a
direct -File invocation and a -Command wrapper.
The SessionStart marker-clear is not a script but an inline `rm -f … || true`,
which is not valid PowerShell, so it gets an explicit translation.
New test/portability/test-powershell-hooks.sh asserts the wiring on any
platform -- every entry resolves to an installed file, .ps1 entries carry the
bypass and the exit-code propagation, and some hooks still run bash -- and
executes the hooks wherever PowerShell is present (pwsh ships on all three
GitHub runner images). README and docs/03 document the flag, the policy
tradeoff, and which hooks stay bash.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVndNviHZSnbKJnWP7jFbV
e6b9724 to
8d2ddde
Compare
|
VERDICT: PASS All six PS1 files ship with their bash siblings, the |
What & why
Split out of #95 at the AI reviewer's request — it blocked that PR as scope creep, correctly. This is that commit unchanged.
PowerShell hook variants behind
--hook-shell powershell. Theshell: "bash"fix earlier in this stack covers Windows with Git Bash; this covers machines without it. Six hooks ship a.ps1sibling, swapped per entry by name. Hooks without a sibling stay bash, which is correct rather than lazy:check-package-availabilityprobes apt/brew andsessionstart-drift-checkis jq-driven, so both are Linux/macOS-shaped by nature.Three Windows traps, each found by running the hooks rather than reading them — worth a reviewer's attention because each one fails silently:
.ps1as ANSI, so a UTF-8 em-dash decoded to a cp1252 smart quote — which PowerShell accepts as a string delimiter — andmicrobit-enforcer.ps1failed to parse. All shipped.ps1are ASCII and BOM-free, enforced by--checkalong with the.shpairing.Restricted, so naming a.ps1directly fails with "running scripts is disabled on this system". An earlier round of my own testing passed only because this session runs with a bypass already in place — the generated command now spawns PowerShell with-ExecutionPolicy Bypass, which applies to that child process running a script the user installed deliberately and never changes machine policy.powershell -Commandcollapses a non-zero child exit to 1, which would have turned aPreToolUseblock (exit 2) into a non-blocking error — the safety hooks would have appeared to work while permitting everything. The command ends with; exit $LASTEXITCODE; exit 2 was then verified to survive both a direct-Fileinvocation and a-Commandwrapper.A
target_path_forrule keyed to.shwould also have routedmicrobit-enforcer.ps1into.claude/skills/and never installed it — caught by asserting every settings entry resolves to a file on disk.Type of change
Scope
core,safety,git-workflow,token-efficiency,commands--hook-shelldefaults tobash, so no persona output changes.Tests
python3 configure.py --checkpasses locally.test/portability/test-powershell-hooks.shasserts the wiring on any platform (every entry resolves to an installed file,.ps1entries carry the bypass and exit-code propagation, some hooks still run bash) and executes the hooks wherever PowerShell is present;pwshships on all three GitHub runner images.CHANGELOG
## Unreleased.License & NOTICE
templates/discipline-skills/untouched by this PR.LICENSE/NOTICEuntouched.Signing
I understand
CONTRIBUTING.md.Fifth of five stacked PRs — based on
feat/audit-open-items(#95).