teardown-host.sh has no [ -t 0 ] gate: a non-interactive run without --yes dies mute #113

Closed
opened 2026-07-19 21:07:12 +00:00 by dan-claude-bot · 0 comments
dan-claude-bot commented 2026-07-19 21:07:12 +00:00 (Migrated from github.com)

Split out of #112, where claude-bot-andresmgsl found it while reviewing the #111 EOF sweep. The EOF half is fixed in #112; this is the other half, and it is a different defect.

What

host/teardown-host.sh:31 asks its confirmation like this:

if [ "$yes" -eq 1 ]; then
  echo "(confirmed non-interactively: --yes/BOX_YES)"
else
  read -rp "Continue? [y/N] " a || { echo "aborted"; exit 1; }
  case "$a" in y|Y) ;; *) echo "aborted"; exit 1 ;; esac
fi

There is no [ -t 0 ] check anywhere before that read. So a non-interactive run without --yes/BOX_YES — a CI job, a pipe, a nohup, anything without a terminal — falls straight into read, gets immediate EOF, and now (post-#112) prints aborted and exits 1.

That is no longer silent, which is what #112 fixed. But it is still the wrong contract. Every other destructive gate in the repo refuses a terminal-less run with a message that says why and points at the override:

  • host/revoke-user.sh:44-47refusing to --purge without a terminal to confirm on. BOX_YES=1 means yes., exit 2
  • bin/box confirm() — same shape, refuses on [ -t 0 ] failing
  • install.sh:54-65 — documents the contract and reads /dev/tty directly

teardown is the most destructive script in the tree — all boxes, both network generations, profiles, firewall units, optionally apt-purging Incus — and it is the one that does not tell a non-interactive caller how to proceed. It just says aborted.

Suggested fix

Mirror revoke-user.sh, which is the closest sibling and already has the right shape:

if [ ! -t 0 ]; then
  echo "teardown-host: refusing to run without a terminal to confirm on. --yes (or BOX_YES=1) means yes." >&2
  exit 2
fi
read -rp "Continue? [y/N] " a || { echo "aborted"; exit 1; }

Note the exit code: 2 for "you invoked this wrong", matching revoke and box's own usage-error convention, versus 1 for "you were asked and you refused".

Why it was not folded into #112

#112 is scoped to the silent-EOF class: an unguarded read under set -e that dies before it can name the abort. That is one token per site and it is now swept repo-wide by test/cli.sh. This is a missing precondition — a different shape, a different exit code, and it changes the refusal contract of a script CI calls (box uninstall --purge-host and the uninstall drill both run teardown unattended, though both pass --yes, so both are unaffected). It deserves its own diff and its own test rather than riding along.

Worth checking whether any caller relies on the current fall-through before changing the exit code.

Split out of #112, where `claude-bot-andresmgsl` found it while reviewing the #111 EOF sweep. The EOF half is fixed in #112; this is the other half, and it is a different defect. ## What `host/teardown-host.sh:31` asks its confirmation like this: ```bash if [ "$yes" -eq 1 ]; then echo "(confirmed non-interactively: --yes/BOX_YES)" else read -rp "Continue? [y/N] " a || { echo "aborted"; exit 1; } case "$a" in y|Y) ;; *) echo "aborted"; exit 1 ;; esac fi ``` There is no `[ -t 0 ]` check anywhere before that `read`. So a non-interactive run without `--yes`/`BOX_YES` — a CI job, a pipe, a `nohup`, anything without a terminal — falls straight into `read`, gets immediate EOF, and now (post-#112) prints `aborted` and exits 1. That is no longer *silent*, which is what #112 fixed. But it is still the wrong contract. Every other destructive gate in the repo refuses a terminal-less run with a message that says why and points at the override: - `host/revoke-user.sh:44-47` — `refusing to --purge without a terminal to confirm on. BOX_YES=1 means yes.`, exit **2** - `bin/box` `confirm()` — same shape, refuses on `[ -t 0 ]` failing - `install.sh:54-65` — documents the contract and reads `/dev/tty` directly teardown is the most destructive script in the tree — all boxes, both network generations, profiles, firewall units, optionally apt-purging Incus — and it is the one that does not tell a non-interactive caller how to proceed. It just says `aborted`. ## Suggested fix Mirror `revoke-user.sh`, which is the closest sibling and already has the right shape: ```bash if [ ! -t 0 ]; then echo "teardown-host: refusing to run without a terminal to confirm on. --yes (or BOX_YES=1) means yes." >&2 exit 2 fi read -rp "Continue? [y/N] " a || { echo "aborted"; exit 1; } ``` Note the exit code: `2` for "you invoked this wrong", matching revoke and box's own usage-error convention, versus `1` for "you were asked and you refused". ## Why it was not folded into #112 #112 is scoped to the silent-EOF class: an unguarded `read` under `set -e` that dies before it can name the abort. That is one token per site and it is now swept repo-wide by `test/cli.sh`. This is a missing *precondition* — a different shape, a different exit code, and it changes the refusal contract of a script CI calls (`box uninstall --purge-host` and the uninstall drill both run teardown unattended, though both pass `--yes`, so both are unaffected). It deserves its own diff and its own test rather than riding along. Worth checking whether any caller relies on the current fall-through before changing the exit code.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/box#113
No description provided.