Repository navigation
v1.0.7: heredoc false positive, one-model data strip
One false positive from v1.0.6, found the same day. The commit guard blocked a Bash heredoc that wrote a file whose text mentioned git commit, a shell wrapper, and git push -f. Heredoc bodies are data, bash never runs them, but they are not quoted, so the old quote strip left them in and the flag checks fired on prose.
The obvious fix, strip heredoc bodies and keep the quote strip, did not survive review. Two strippers with per-line state left several ways to hide a real force-push behind an innocuous first line. So this release replaces both with one thing, and then closes every way I could find for bash and the guard to read the same command differently.
One walk, one rule
- A single character walk strips all data before anything is classified, and it keeps its state across lines: quoted spans in all three styles,
$(...)and backticks that reopen code inside double quotes and inside unquoted heredoc bodies,${...}parameters, arithmetic where<<is a shift and|is an operator, array subscripts, test brackets, comments, and heredoc bodies with any delimiter. Real command boundaries are a byte no command contains, so a;or&that is data can never split a segment. - The rule the code follows, and the header states: the walk must never believe it is in data where bash is in code, and no check may depend on the words next to it where bash may leave nothing. Where a construct cannot be classified, it is refused with a reason instead of guessed.
What it catches that v1.0.6 did not
- A force-push,
--no-verify, or-nsplit off itsgitby an array subscript, a quoted default that contains), an escaped separator, an empty quote or expansion, or a bare$varglued to the flag. - A shell wrapper with an expansion before its option (
bash $a -c,bash "${a[@]}" -c,sh $(:) -c), fed from stdin, a here-string, or a pipe, or throughenv -S. - A parameter default that expands to a flag or a
+refspec(${x:---force},${x:-+main}), quoted or not. - A committer or author set on the command line, in the environment, or by
git configin the same call, including a value or key built from a substitution. - Git config pointed at a file the guard cannot read (
--config-env, theGIT_CONFIG_*family,include.path,HOME,XDG_CONFIG_HOME,core.hooksPath). - A working-tree secret committed through a pathspec that a multi-word message, a short flag cluster, or a redirection had hidden.
- An unquoted pattern that bash expands into a flag when the command runs (
-*with a file named--no-verifyin the directory,--{force,}).
Hygiene
- The hook fails closed if the walk itself fails, pins
LC_ALL=Cso a bad byte cannot make a tool drop the rest of the input, disables its own globbing, passes every grep pattern with-e, reads the secrets diff as text with no textconv, no relative paths, and no color, and caps commands at 256 KB, diffs at 16 MB, and segments at 500. - 585 guard cases, up from 73. Most of the new ones are bypass attempts, each verified to run the dangerous command under bash 3.2 before being pinned. Green on macOS awk, mawk, and busybox awk. The guard needs git 2.28 or newer.
Install: copy hooks/guard-commit.sh again.