Requirement 7's read bound in host-setup/agent-safety/claude/gh-write-guard.py (_reads_its_input) credits a while loop whose condition's first word is read over a descriptor-0 redirect. It does not check that this read's exit status is the condition's status, or that the read actually consumes the loop's input. Each shape below is allowed by _unbounded_wait_loop and ran until killed (exit 124 under a short timeout) against a two-line regular file f:
The condition's status is not the read's:
while read l || true; do sleep 30; done < f
while read l; true; do sleep 30; done < f
The read is fed by its own redirect, not the loop's, so it never exhausts:
while read l < f; do sleep 30; done < f
while read l <<< x; do sleep 30; done < f
- An
exec < f in the body rebinds descriptor 0 on every pass in the same way.
The read succeeds without consuming anything (only -u is rejected today):
while read -t 0 l; do sleep 30; done < f, and the clustered read -rt0 spelling
while read -n 0 l; do sleep 30; done < f
while read -N 0 l; do sleep 30; done < f (bash 4 and later)
Root cause: the check asks whether a read comes first rather than whether the condition ends when that loop's own input is exhausted. Denying anything past a bare read [options] NAME... condition with no redirect of its own, and denying the non-consuming options, is the safe direction, as the piped-read false deny already is.
Found by a local strict review pass on the auto-1633 lane (#2159, fixing #1633) and recorded here rather than fixed, since it predates that change.
Requirement 7's
readbound inhost-setup/agent-safety/claude/gh-write-guard.py(_reads_its_input) credits awhileloop whose condition's first word isreadover a descriptor-0 redirect. It does not check that thisread's exit status is the condition's status, or that thereadactually consumes the loop's input. Each shape below is allowed by_unbounded_wait_loopand ran until killed (exit 124 under a shorttimeout) against a two-line regular filef:The condition's status is not the
read's:while read l || true; do sleep 30; done < fwhile read l; true; do sleep 30; done < fThe
readis fed by its own redirect, not the loop's, so it never exhausts:while read l < f; do sleep 30; done < fwhile read l <<< x; do sleep 30; done < fexec < fin the body rebinds descriptor 0 on every pass in the same way.The
readsucceeds without consuming anything (only-uis rejected today):while read -t 0 l; do sleep 30; done < f, and the clusteredread -rt0spellingwhile read -n 0 l; do sleep 30; done < fwhile read -N 0 l; do sleep 30; done < f(bash 4 and later)Root cause: the check asks whether a
readcomes first rather than whether the condition ends when that loop's own input is exhausted. Denying anything past a bareread [options] NAME...condition with no redirect of its own, and denying the non-consuming options, is the safe direction, as the piped-readfalse deny already is.Found by a local strict review pass on the
auto-1633lane (#2159, fixing #1633) and recorded here rather than fixed, since it predates that change.