Development record
The kill-by-search and by-owner readings (iss-2609251640452031) left spellings of a kill by name or selector unseen. The ordinary ones are read on fix/drain-guard (4cc129009): a brace or paren group ({ pgrep make; } | xargs kill), a search inside a shell string (kill $(sh -c 'pgrep make')), a kill inside a shell string xargs runs or one that reads the pipe (pgrep make | xargs -I{} sh -c 'kill {}', pgrep make | xargs bash -c 'kill "$@"' _, pgrep make | sh -c 'xargs kill'), a kill behind xargs and an unknown launcher (pgrep make | xargs myrunner kill, now a Tier 2 warn), a selector value attached with a byte other than a letter or digit (pkill -tpts/3, pkill -ubob.smith), and pkill -Ubob or -Gstaff attached, read once pkill's signal word is read as a signal first.
The kill-by-search and by-owner readings (iss-2609251640452031) left spellings of a kill by name or selector unseen. The ordinary ones are read on fix/drain-guard (4cc129009): a brace or paren group ({ pgrep make; } | xargs kill), a search inside a shell string (kill $(sh -c 'pgrep make')), a kill inside a shell string xargs runs or one that reads the pipe (pgrep make | xargs -I{} sh -c 'kill {}', pgrep make | xargs bash -c 'kill "$@"' _, pgrep make | sh -c 'xargs kill'), a kill behind xargs and an unknown launcher (pgrep make | xargs myrunner kill, now a Tier 2 warn), a selector value attached with a byte other than a letter or digit (pkill -tpts/3, pkill -ubob.smith), and pkill -Ubob or -Gstaff attached, read once pkill's signal word is read as a signal first.
Three remain, and each waits on ruling AS: (1) a pid list carried through a file across commands (pgrep make > p; xargs kill < p), a data flow between lines the per-line reading does not follow; (2) a pid list taken from a ps | grep | awk chain, where reading ps or grep as a search source would also block ps -o pid= -p N | xargs kill, the recorded-pid kill; (3) a kill by the holder of a port or file, kill $(lsof -t -i :8080) and fuser -k, which stop whatever holds it, a peer session's server included, and have no entry and no statement in the brief. A pid list carried through a variable or a while-read loop (pids=$(pgrep make); kill $pids) is not this record's: it is the plain-variable half of iss-2609251824244354, ruled in dcn-2609250000000016 (c) and deferred there.
Remedy grounds (2026-09-29)
- Why: the three open spellings are scope choices the record names, so each answer is written with its entry and its test; ruling AS is unanswered and none is picked.
- Sources (consulted 2026-09-29): fuser(1), which documents -k as killing every process accessing the file and 'fuser -km /home' as killing everything on that file system (https://man7.org/linux/man-pages/man1/fuser.1.html), so a kill by holder is a kill by selector in effect; the Claude Code sandboxing documentation treats command rules and operating-system isolation as 'complementary layers' (https://code.claude.com/docs/en/sandboxing), which is why an uncovered spelling is stated as a limit rather than left silent.
- Rejected: reading ps or grep as a search source unconditionally, which blocks the recorded-pid kill the record names as everyday.
Frontmatter
| id | iss-2609262259360005 |
|---|---|
| type | issue |
| lifecycle | resolved |
| severity | minor |
.abcd/work/issues/resolved/iss-2609262259360005-kill-readings-leave-variable-file-brace.md
open on GitHub ↗ · commit history ↗
mentions6
- dcn-26092500000000162026-09-25 — Correction to the two entries above on the shell guard's unknown word (fix round 4 of lane guard, run A, closing review4-guard). Over-block (2) of the round-3 entry is wider than it says: any command word whose basename ends in a substitution (`$(date) x`, `"$(which go)" build ./...`, `$(command -v X) anything`, `/usr/bin/$(x) y`) followed by any one operand blocks, because killall-by-name and pkill-by-pattern name only a program and an operand; and the verdict carried killall's own lesson ("stop it by pid"). Where the only entries a command fires are ones its unknown program name can be, the verdict is now reported under the reserved id `program-name-unknown`, in the substitution family, naming the entry the line reads as and the way past, spelling the program's name; the entries stay in its matches, and an entry the line does name outranks it. A narrower `$(which NAME)`/`$(command -v NAME)` reading was weighed and not taken. Three further rulings of the round. (a) A line the tokenizer cannot split is BLOCKED on the hook (`command-unparsable`), not failed open: where the tokenizer is right no shell runs the line either, so the block costs nothing, and where it is wrong (the `$'\c'` misreading this round fixed, which swallowed the string's closing quote) a fail-open is a bypass of every blocker by construction. itd-103 ac-1 names the binary failing to run, which still fails open loudly, as do an unreadable payload and a registry that will not load; the check verb keeps reporting the error as a fault (exit 2); the 961-line repository corpus holds no unparsable line. (b) An unquoted here-document body is read as bash expands it: the command substitutions in it are commands of the redirecting command's chain and its text is data, and a delimiter written with a backslash (`<<\EOF`, `<<E\OF`) is quoted, as bash reads it. (c) A `${…}` in which a substitution's output lands is unknown from its `${` on, so a word that is wholly such an expansion falls under allow (1) of the round-2 entry exactly as `"$(x)"` does, and `git push "${X:-$(echo --force)}" origin main` is not seen. The plain-variable half (`$X`, `${X:-git}`, a `${…}` holding no substitution) stays iss-2609251824244354, deferred with its reason; it is not seen as the program name, as a flag, or inside a payload.
- dcn-26092700000000012026-09-27 — The kill-by-search reading follows a search into and out of shell strings, and keeps one over-block, recorded so it is not mistaken for a defect (lane drainG2, autonomous run A, on iss-2609262259360005 and the review of the first reading). Every command of a string that `xargs` runs is read as handed xargs's input, so `pgrep make | xargs sh -c 'kill 4242'` blocks as `kill-by-search` though its kill names a pid: `xargs -I{}` replaces the input into any part of the string, and telling a command that reads `"$@"` or `$1` from one that does not would be a text match on the string, which the feed mechanism does not make. The same holds for the standard input a shell passes to the commands of its string (`pgrep make | sh -c 'xargs kill'`). The accepted over-block of the first reading, that a lower-case signal name (`pkill -term -g <pgid>`) blocked as `pkill-by-owner` because its letters read as the `-t` and `-u` selectors, is removed rather than recorded: `pkill`'s first `-NAME` word naming a signal is read as its signal, in any case and with or without `SIG`, as procps-ng and BSD pkill read it, which is also what lets `-U` and `-G` be read attached (`pkill -Ubob`) without taking `-HUP`, `-USR1` or `-SIGTERM` for them. Only the first such word is the signal, so `pkill -9 -term -g <pgid>` still blocks: both implementations hand the second word to their option parser as `-t erm`. `killall` is not read this way, because psmisc killall reads a signal name only when it begins with a capital letter and parses `killall -term` as `killall -t erm`.
- dcn-26092700000000042026-09-27 — The kill-by-search reading follows a search into and out of shell strings, and keeps one over-block, recorded so it is not mistaken for a defect (lane drainG2, autonomous run A, on iss-2609262259360005 and the review of the first reading). Every command of a string that `xargs` runs is read as handed xargs's input, so `pgrep make | xargs sh -c 'kill 4242'` blocks as `kill-by-search` though its kill names a pid: `xargs -I{}` replaces the input into any part of the string, and telling a command that reads `"$@"` or `$1` from one that does not would be a text match on the string, which the feed mechanism does not make. The same holds for the standard input a shell passes to the commands of its string (`pgrep make | sh -c 'xargs kill'`). The accepted over-block of the first reading, that a lower-case signal name (`pkill -term -g <pgid>`) blocked as `pkill-by-owner` because its letters read as the `-t` and `-u` selectors, is removed rather than recorded: `pkill`'s first `-NAME` word naming a signal is read as its signal, in any case and with or without `SIG`, as procps-ng and BSD pkill read it, which is also what lets `-U` and `-G` be read attached (`pkill -Ubob`) without taking `-HUP`, `-USR1` or `-SIGTERM` for them. Only the first such word is the signal, so `pkill -9 -term -g <pgid>` still blocks: both implementations hand the second word to their option parser as `-t erm`. `killall` is not read this way, because psmisc killall reads a signal name only when it begins with a capital letter and parses `killall -term` as `killall -t erm`.
- iss-2609251640452031The kill-by-pattern entries leave four spellings of a kill by name or selector uncovered: kill handed the output of pgrep in a command substitution, pgrep piped into xargs kill, pkill selecting by user with -u, and pkill selecting by terminal with -t. The first two reach the pattern through a second command the entries do not read; the last two are value flags that consume the selector, so no operand remains, and a kill by user is every session of that user. The same holds when the selector is a command substitution, which fills the value flag's slot and leaves no operand: `pkill -u $(whoami)` (every session of the caller) and `pkill -g $(cat p)` are allowed by design, and review2-guard finding 8 names both. The commit that added the entries named the gap; no record did. Found by review-guard finding 5.
- 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.
- iss-2610112214257996Two spellings of a kill by name that iss-2609262259360005 named stay unread after ruling AS (product thinker, 2026-10-11) settled its third, the kill by the holder of a port or file: (1) a pid list carried through a file across commands (pgrep make > p; xargs kill < p), a data flow between commands the per-line reading does not follow; and (2) a pid list taken from a ps | grep | awk chain, where reading ps or grep as a search source would also block ps -o pid= -p N | xargs kill, the everyday kill of a recorded pid. The ruling as relayed to lane G2a named only the holder class, so whether these two block is still owed. A pid list carried through a variable is iss-2609281134544802.