Yesterday’s closing-keyword work found that the guard hook’s shared parser splits a command at an escaped newline. Bash joins a line that ends in a backslash to the next one, and PowerShell does the same with a backtick; shlex does neither. It kept the newline as text, glued to a neighbouring word or as a word of its own, and the parser took that word for a command separator. So git stash with pop on the next line got past the stash guard, gh pr merge 12 with --squash on the next line got past the merge guard, and every PowerShell form passed. Of 23 shapes I tried against main, 15 came out wrong. Two were wrong the other way round: git stash push -u with its -m tag on the next line was refused as an untagged stash.
The fix is one function that joins continued lines as the shell does, run before anything reads the command line by line. Most of the work was in what it must leave alone, and I asked the shells rather than my memory. In Git Bash a backslash at the end of a comment does not continue it, a backslash before a CRLF does, and a heredoc’s body starts after the whole continued line. In PowerShell a backtick before a newline inside a double-quoted string is the newline itself. Each answer became a test, and I broke each safeguard in turn to watch its test fail. The heredoc case mattered most. Joining after the heredoc bodies are stripped, the obvious place, glues the line after a body onto the operator’s line and hides it. Joining without knowing where the bodies are lets a body line that ends in a backslash, a Windows path for instance, swallow its terminator and every command after it. The closing-keyword reader had joined continuations on its own since yesterday. It now relies on the shared join, and the one case its own join got wrong, a backtick-newline inside a PowerShell string, reads as the newline PowerShell makes of it.
Before trusting it I replayed every Bash and PowerShell command in the retained transcripts, 20,634 of them, through the old and the new hook. Only 146 contain a continuation, and the old parser read 127 of those differently from the shell. In 102 a command’s name was a flag or began with a newline, like \ngcloud or \npython, which no guard could ever match. The new parser has none of those, and every command without a continuation comes back byte for byte. No verdict changed: none of the misread commands was one a guard watches, and nothing that passed before is refused now. Two older gaps in the same parser turned up along the way and are left for a follow-up task. A <<EOF inside quotes or a comment opens a heredoc that is not there and hides every line after it, and in cat <(git stash pop) the parser reads <( as a redirect and drops git.