Skip to content

os-verify-lock.sh reads the & in a 2>&1 redirection as a background operator, downgrading a correctly &&-joined command to batch-last-exit #12518

Description

@claude

scripts/pm/os-verify-lock.sh's certifiability scanner treats the & in a 2>&1redirection as a background operator, so a correctly &&-joined command is downgraded from VERDICT command-exit to VERDICT batch-last-exit with the "⚠ THIS COMMAND'S EXIT CODE CANNOT CERTIFY IT" banner.

Found incidentally while running the derived gate union for #11969 (ADR conversion); unrelated to that card's surface, filed rather than fixed.

Measured, isolated to one variable

Same joiner, same trivial parts, run in this container against the real script:

$ bash scripts/pm/os-verify-lock.sh -c 'true && true'
os-verify-lock: VERDICT command-exit 0 · held the lock 0s · waited 59s
$ bash scripts/pm/os-verify-lock.sh -c 'true > /dev/null 2>&1 && true'
os-verify-lock: ⚠ THIS COMMAND'S EXIT CODE CANNOT CERTIFY IT — a part of it is backgrounded with '&'.
os-verify-lock: VERDICT batch-last-exit 0 · ⚠ NOT A VERDICT ON THE WHOLE COMMAND — a part of it is
backgrounded with '&', so this number is the LAST part's exit and a failure in an earlier part is
NOT in it · join the parts with '&&' to get a verdict that covers all of them · held the lock 0s

The only difference between the two is the 2>&1. Nothing is backgrounded in either.

Mechanism

The scanner's '&' case reads exactly one character ahead:

'&')
# `&&` is the one joiner that certifies, so it is consumed, not flagged.
nx="${s:i+1:1}"if [[ "$nx"=='&' ]];then
i=$((i +2))continuefi((depth ==0))&& {
CERTIFIABLE_NOTE="a part of it is backgrounded with '&'"return 1
}
;;

In 2>&1 the & is preceded by > and followed by 1, so nx is 1, the && branch is not taken, and it falls through to the backgrounding flag. There is no case for the >& / >>& / N>&M redirection forms. The look-ahead is the wrong direction for this token: what disambiguates it is the character before the &, not the one after.

Why it is worth fixing rather than tolerating

  1. The remedy it prints is already satisfied, so an agent cannot act on it. The banner says "join the parts with '&&' instead and the verdict certifies every one of them" — the command is&&-joined. Following the advice cannot clear the warning, which is the shape that teaches agents to ignore the banner generally, including on the real ;/|/& cases where it is correct.
  2. It misfires on the pattern this repo's own gate docs recommend. Several gates print "piping this gate reports the PIPE's status … use redirect-then-capture" precisely to avoid reading tail's exit code. cmd > out.log 2>&1 && next is that recommended form, and it is exactly the form that trips this.
  3. It degrades a true positive into noise.batch-last-exit is documented as the honest label for an uncertifiable command; applying it to a certifiable one inverts the signal the label exists to carry.

Same family as a closed precedent

#10570guard-main-checkout-bash.sh read a > inside a shell comment as a redirect and false-blocked the command. Same root shape: a single-character shell-token scan that does not model the surrounding context. Different script, so not a duplicate, but the fix and the self-test shape should probably rhyme.

Suggested shape (not prescribing)

Consume the redirection forms before the background check — on seeing &, look back for an unescaped > (optionally preceded by a digit or >), and treat that as a redirect rather than a joiner. scripts/pm/os-verify-lock.sh already carries an st_case self-test suite covering the ;, |, || and & verdicts (around the VERDICT batch-last-exit cases), so a regression pin belongs there: assert command-exit for true > /dev/null 2>&1 && true and that a genuine sleep 1 & still earns batch-last-exit.

Filed unassigned for triage; no fix attempted here.


Generated by Claude Code


Generated by Claude Code

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions