fix(hooks): escape cmd_* answers for the quoting context they land in - #99
Open
tigers1997 wants to merge 1 commit into
Open
tigers1997 wants to merge 1 commit into
tigers1997 wants to merge 1 commit into
Conversation
A check command containing a double quote was interpolated raw into the generated Stop hook's "label|<command>" bash array element. The quote closed the element early and the whole CHECKS=(...) array stopped parsing, so the hook died at load with a syntax error, emitted no report, and left a check loop that looks installed. Reported from a downstream project with: cmd_lint = python -c "import ast,io,glob; [ast.parse(...)]" Silent failure is the worst outcome for a check loop, and the quieter half of the same bug is worse still: a command with an embedded quote but no parentheses parses without error and splits into several array elements, so the check runs truncated and nothing complains. The .ps1 variant had the bug mirrored — a single quote breaks the single-quoted PowerShell hashtable value — but nobody had hit it yet. substitute_placeholders() now takes an escaper chosen per target extension: backslash, ", $ and backtick for the double-quoted bash literal in stop-run-checks.sh; ' doubled for the single-quoted PowerShell literal in stop-run-checks.ps1. Prose targets (CLAUDE.md) keep the raw answer, so the escaping cannot leak into documentation. Escaping applies at interpolation only: eval "$run_cmd" still expands $VAR and $(...) in the command at run time, as it always did. test-command-quoting.sh scaffolds with a command carrying ", ', $, backtick, backslash, parens and a semicolon, then asserts both hooks parse and that all three commands round-trip byte-exact out of the rendered literal — escaping that mangles the command is no better than escaping that breaks the parse. Reverting the fix reproduces the reported syntax error verbatim. The PowerShell probe skips cleanly where no PowerShell is on PATH. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ke9nVhgkkvmXF55iv5nopf
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reported downstream: a
cmd_lintcontaining a double quote produced a Stop hook that was dead on arrival.interpolated raw into the
"label|<command>"bash array element:The quote closes the element early and the whole
CHECKS=(...)array stops parsing. The hook then fails at load, emits no report, and leaves a check loop that looks installed — the worst failure mode for a Stop hook, because nothing surfaces it.The quieter half
A command with an embedded quote but no parentheses parses without error and silently splits into several array elements, so the check runs truncated and nothing complains at all. Same root cause, no diagnostic.
The
.ps1variant had the bug mirrored — a single quote breaks the single-quoted PowerShell hashtable value instop-run-checks.ps1— but nobody had hit it yet.Fix
substitute_placeholders()now takes an escaper selected per target extension:.sh\"$backtick.ps1'doubledProse targets keep the raw value, so escaping cannot leak into
CLAUDE.md.Escaping applies at interpolation only.
eval "$run_cmd"still expands$VARand$(...)in the command at run time, exactly as before — a command that wants shell expansion still gets it.Test
test/stop-run-checks/test-command-quoting.sh, picked up by the existingtest/stop-run-checks/test-*.shglob incheck.yml.Scaffolds with commands carrying
",',$, backtick,\, parens and a semicolon, then asserts:bash -n;Parser::ParseFilefor the.ps1)CLAUDE.mdstill receives the raw command, pinning the per-extension behaviorReverting the fix reproduces the reported syntax error verbatim. The PowerShell probe skips cleanly where no PowerShell is on PATH (
pwshships on all three runner images).Verification
Full suite, working tree vs. a clean
HEADworktree:Identical otherwise — no test changed status. The 29 failures are pre-existing environment failures on this Windows box (Docker-dependent container checks, cygwin fork errors) and reproduce on
HEAD.configure.py --checkpasses; no drift intemplates/orexamples/.Note for anyone already scaffolded
The retrofit path is non-destructive by design, so a re-run stages the corrected
stop-run-checks.shto.claude-retrofit/incoming/and leaves the broken file live. Promote it, or re-run with--on-collision=overwrite, to actually pick up the fix — thenbash -n .claude/hooks/stop-run-checks.shto confirm.🤖 Generated with Claude Code
https://claude.ai/code/session_01Ke9nVhgkkvmXF55iv5nopf