Development record
The shell guard blocks valid bash as command-unparsable when a double-quoted ${…} carries double quotes of its own. Inside double quotes bash parses a parameter expansion to its own closing brace: a double quote in it opens a nested string instead of closing the outer one, and a single quote pairs with the next. The tokenizer's double-quote branch and closingDoubleQuote (internal/core/guard/tokenize.go) ended the string at the first nested quote, so an apostrophe inside the nested quotes read as an unterminated single quote: echo "${MSG:-"don't"}", "${1:-"it's"}", printf '%s\n' "${NAME:-"O'Brien"}" and "${X//"'"/x}" returned ErrUnparsableCommand, which the pre-tool-use hook (internal/surface/cli/guard.go) now blocks as command-unparsable, where the round before they ran (review5-guard finding 2).
The shell guard blocks valid bash as command-unparsable when a double-quoted ${…} carries double quotes of its own. Inside double quotes bash parses a parameter expansion to its own closing brace: a double quote in it opens a nested string instead of closing the outer one, and a single quote pairs with the next. The tokenizer's double-quote branch and closingDoubleQuote (internal/core/guard/tokenize.go) ended the string at the first nested quote, so an apostrophe inside the nested quotes read as an unterminated single quote: echo "${MSG:-"don't"}", "${1:-"it's"}", printf '%s\n' "${NAME:-"O'Brien"}" and "${X//"'"/x}" returned ErrUnparsableCommand, which the pre-tool-use hook (internal/surface/cli/guard.go) now blocks as command-unparsable, where the round before they ran (review5-guard finding 2).
Grounds
- pursued: echo "${MSG:-"don't"}" and its kin allow on Check and exit 0 on the hook, while a hazard after the string or in a substitution inside the expansion blocks; it would be shown wrong by a line bash runs whose substitution the guard does not block, as the 35-shape differential against bash 3.2 and 5.3 checks
Frontmatter
| id | iss-2609252214217550 |
|---|---|
| type | issue |
| lifecycle | resolved |
| severity | minor |
.abcd/work/issues/resolved/iss-2609252214217550-the-shell-guard-blocks-valid-bash-as.md
open on GitHub ↗ · commit history ↗
mentions1
- dcn-26092500000000172026-09-25 — Correction to the round-4 entry above on the shell guard's here-document bodies, with three further rulings (fix round 5 of lane guard, run A, closing review5-guard). Ruling (b) of that entry read an unquoted body line by line, and the round's report held that bash can only end such a body later than the guard, so the guard over-reads and never under-reads. That is false: in a body whose delimiter is unquoted, bash joins a physical line ending in an odd number of backslashes with the next line before it compares the delimiter, so `x\` followed by `EOF` does not end the body, and ending it there read the rest as command text, where a single-quoted span or a `#` hid a substitution the body runs (iss-2609252214137586). A later end read with a different grammar is an under-read. The body is now read by the logical lines bash compares: joined across an odd trailing run of backslashes, for an unquoted delimiter only; an even run and a quoted delimiter join nothing, and a `<<-` body strips the leading tabs of the joined line alone. (a) Inside double quotes a `${…}` ends at its own `}`, found as bash's parser finds it: a `"` in it opens a nested string, a `'` pairs with the next `'` for the parse only (the substitutions between them still run, and are followed), a bare `{` opens nothing. `echo "${MSG:-"don't"}"` and its kin, which blocked as `command-unparsable` after round 4, run; a `${` that never closes leaves the string as the text it was, and BLOCK stays the answer for a line the tokenizer cannot split (iss-2609252214217550). (c) A word that is wholly `"$(cat <<DELIM … DELIM)"`, whose body the shell does not change (a quoted delimiter, or an unquoted one with no `$`, backtick or backslash in it), is ALSO read as the document's text wherever it is a payload (`sh -c`, `eval`, `su -c`, `env -S`), beside the unknown reading, which keeps every signal it raises: a clean document still warns as an uninspectable payload, and a hazard in the document blocks under its own entry. Replacing the unknown reading was weighed and not taken, because a `cat` redefined earlier in the same line would print something else, and an additive reading cannot weaken a verdict (iss-2609252214215409). (d) One more accepted over-block, named so it is not mistaken for a defect: a here-document whose delimiter is a substitution (`cat <<$(echo EOF)`) is not run to learn the delimiter, so no line ends the document and the line blocks as `heredoc-unterminated`, although bash runs it; writing the delimiter out is the way past.