From f49f8571716571b8ff82597d86f7235368fe1c48 Mon Sep 17 00:00:00 2001 From: Sebastian Mendel Date: Wed, 22 Jul 2026 12:47:05 +0200 Subject: [PATCH] docs(troubleshooting): guard `timeout` and validate capability probes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `timeout` is GNU coreutils and absent from a stock macOS, so a hard-coded `timeout 2 --version` fails outright with "command not found" and yields empty output; wrapped in the usual `|| true`, that reads downstream as "the tool produced nothing". Symptom: a step green on ubuntu-latest, failing on macos-latest with the tool reported absent. Document a run_bounded helper (timeout → gtimeout → unbounded), noting that ` --- .../cli-tools/references/troubleshooting.md | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/skills/cli-tools/references/troubleshooting.md b/skills/cli-tools/references/troubleshooting.md index 2a78906..5daed03 100644 --- a/skills/cli-tools/references/troubleshooting.md +++ b/skills/cli-tools/references/troubleshooting.md @@ -115,3 +115,55 @@ When wrapping package-manager commands for unattended runs: 4. **Diagnose a stuck run** via `/proc//fd` (fd 0 → `/dev/pts/*` with fd 1/2 → `/dev/null` = waiting on an invisible prompt) and `/proc//wchan` (`wait_woken` ≈ tty read). + +## `timeout` Is Not Portable — Guard It + +`timeout` is GNU coreutils. A stock macOS does **not** ship it (Homebrew +coreutils provides `gtimeout`), so a hard-coded `timeout 2 --version` +does not merely lose its time bound — the whole command fails with +`timeout: command not found`, producing **empty output**. Wrapped in the usual +`|| true` / `2>/dev/null`, that failure is silent, and every downstream check +sees "the tool produced nothing" rather than "the guard is missing". Symptom on +a CI matrix: a step that works on `ubuntu-latest` and fails on `macos-latest` +with the tool reported as absent or unversioned. + +Resolve it once and reuse: + +```bash +run_bounded() { # run_bounded SECONDS CMD... + local secs="$1"; shift + if command -v timeout >/dev/null 2>&1; then timeout "$secs" "$@" + elif command -v gtimeout >/dev/null 2>&1; then gtimeout "$secs" "$@" + else "$@" # no bound available — still correct, just unbounded + fi +} +``` + +Falling back to an unbounded run is the right default: ` completion bash`, ` --version`, ` config get …`), a +non-zero exit is not the only failure mode, and neither is empty output. A tool +that does not recognise the subcommand may **treat your probe words as +arguments and do real work**: `bandit complete bash` runs a security scan over +paths named `complete` and `bash`, exits 0, and prints a report that contains +the word "complete" — passing any check that merely greps for a keyword. + +So: + +1. **Validate the shape of what came back**, not just that something did. For a + bash completion script, require an actual registration + (`complete -…`, `compgen `, `COMPREPLY`) rather than the substring + `complete`. +2. **Run probes from a scratch directory** with stdin detached, so a + misinterpreted argument cannot match real files or consume input. +3. **Confirm the result refers to the tool you probed.** A wrapper can return + its host's answer: `rga --generate complete-bash` forwards to ripgrep and + returns ripgrep's script verbatim, and a `gh` extension's `completion` + subcommand can emit `gh`'s own completion. Installing either under the + wrapper's name shadows the host tool.