Development record
2026-09-25 — The shell guard's unknown-word reading and the allows it deliberately keeps (fix round 2 of lane guard, run A, closing review2-guard). What a command substitution prints is not in the command line, so the tokenizer marks where it goes and a word holding one is an unknown word (`internal/core/guard/unknown.go`), failing closed in every role it could play: led by a dash it is every flag its known text can still become, after a value flag it is that flag's value, and as an operand it is one operand of unknown value. Four allows stay, each named so it is not mistaken for a miss. (1) A word that is WHOLLY a substitution is read as an operand, never as a flag: that is how an everyday command spells its commit message and its branch (`git commit -m "$(cat msg)"`, `git push -u origin "$(git branch --show-current)"`), and reading it as every flag would refuse both, so `git push $(printf -- --force) origin main` is not seen. (2) An operand's `+` refspec prefix is read from its known text only, for the same reason: `git push origin $(echo +main:main)` is not seen. (3) A commit or push after `git config core.hooksPath <dir>` in the same line is allowed: the guard reads configuration a command carries for itself (`-c`, `--config-env`, `GIT_CONFIG_*`), not configuration an earlier command writes to a file, and refusing every `git config core.hooksPath` would refuse the ordinary one-time hook setup a repository documents; the one-command spellings block (iss-2609251640464212). (4) Parameter expansions (`--$X`) and a substitution in command position are not yet unknown words; that is iss-2609251824244354, deferred with its reason. Two over-blocks are accepted in the other direction: a short flag's attached value holding a substitution (`git commit -m"$(cat msg)"`) reads as every short flag, because the guard does not know which short options take a value; and `… | xargs bash` reads as a shell reading the pipe, though xargs hands it arguments.
2026-09-25 — The shell guard's unknown-word reading and the allows it deliberately keeps (fix round 2 of lane guard, run A, closing review2-guard). What a command substitution prints is not in the command line, so the tokenizer marks where it goes and a word holding one is an unknown word (internal/core/guard/unknown.go), failing closed in every role it could play: led by a dash it is every flag its known text can still become, after a value flag it is that flag's value, and as an operand it is one operand of unknown value. Four allows stay, each named so it is not mistaken for a miss. (1) A word that is WHOLLY a substitution is read as an operand, never as a flag: that is how an everyday command spells its commit message and its branch (git commit -m "$(cat msg)", git push -u origin "$(git branch --show-current)"), and reading it as every flag would refuse both, so git push $(printf -- --force) origin main is not seen. (2) An operand's + refspec prefix is read from its known text only, for the same reason: git push origin $(echo +main:main) is not seen. (3) A commit or push after git config core.hooksPath <dir> in the same line is allowed: the guard reads configuration a command carries for itself (-c, --config-env, GIT_CONFIG_*), not configuration an earlier command writes to a file, and refusing every git config core.hooksPath would refuse the ordinary one-time hook setup a repository documents; the one-command spellings block (iss-2609251640464212). (4) Parameter expansions (--$X) and a substitution in command position are not yet unknown words; that is iss-2609251824244354, deferred with its reason. Two over-blocks are accepted in the other direction: a short flag's attached value holding a substitution (git commit -m"$(cat msg)") reads as every short flag, because the guard does not know which short options take a value; and … | xargs bash reads as a shell reading the pipe, though xargs hands it arguments.
Frontmatter
| id | dcn-2609250000000014 |
|---|---|
| type | decision |
| date | 2026-09-25 |
.abcd/work/decisions/dcn-2609250000000014-the-shell-guard-s-unknown-word-reading.md
open on GitHub ↗ · commit history ↗
mentions2
- iss-2609251640464212The no-verify blockers miss the same bypass spelled as configuration: a commit or push that points core.hooksPath elsewhere for that one command, through -c, --config-env or the GIT_CONFIG environment, skips the repository hooks exactly as the no-verify flag does, and the matcher steps the -c value over unread. Found by review-guard finding 6 (pre-existing).
- iss-2609251824244354The shell guard reads a command substitution's output as an unknown word, but not a parameter expansion's. A dash glued to a variable (git push --$X origin main, rm -$F after a cd) is read as the literal text --$X, which names no flag, so every blocker allows it while its --$(echo x) twin blocks; and ${GIT:-git} standing in command position is compared as text. bash builds the hazard from either. Found while closing review2-guard (its finding 1 names --$X as pre-existing). The record named a second half, a substitution standing in command position, which fix round 3 of lane guard resolved (a name a substitution prints is every program its known tail allows); this record is the parameter-expansion half alone. Fix round 4 made a ${…} that carries a command substitution (${X:-$(…)}, --${X:-$(…)}) unknown from its ${ on (iss-2609252120211621). What remains is exactly a parameter expansion with no substitution in it: $X, ${X}, ${X:-word} and the other operators with literal words, and $@ / $*, standing as the command's program name, as a flag or glued to one, as an operand an entry constrains, or inside a payload the guard reads.