From d5c907f07e5076762aadf3b3c665776052aafdee Mon Sep 17 00:00:00 2001 From: chhzh123 Date: Thu, 9 Jul 2026 11:01:36 -0700 Subject: [PATCH] Fix bash 3.2 empty-array crash that aborts hooks under `set -u` On macOS (bash 3.2.57, the system default) expanding a bare `"${arr[@]}"` on an empty array under `set -u` is a fatal "unbound variable" error, not the no-op it is on bash >= 4.4. The RLCR hooks run under `set -euo pipefail` and source these files, so that abort exits the hook with a non-zero, non-blocking status and no stderr -- surfaced by Claude Code as "Stop hook error: Failed with non-blocking status code: No stderr output", firing on every Stop-hook invocation. Two empty-able array expansions hit this: - hooks/lib/template-loader.sh: render_template() runs `env "${env_vars[@]}" awk ...`; env_vars is empty for every static block/status/completion message (no substitution vars), so the Stop hook crashed before Codex even ran. - hooks/loop-codex-stop-hook.sh: CODEX_DISABLE_HOOKS_ARGS is initialized empty and only filled when the installed Codex CLI supports `--disable`; on older Codex builds it stays empty, so the `codex review`/`codex exec` invocations crashed the same way. Guard each with the portable `${arr[@]+"${arr[@]}"}` idiom (expand to the quoted elements when non-empty, to nothing when empty), correct on bash 3.2 and >= 4.4 alike. Other `"${arr[@]}"` expansions in the hooks iterate hardcoded non-empty literals and are unaffected. Reproduced on /bin/bash 3.2.57: set -euo pipefail; a=(); env "${a[@]}" true # -> a[@]: unbound variable (exit 1) set -euo pipefail; a=(); env ${a[@]+"${a[@]}"} true # -> ok (exit 0) Co-Authored-By: Claude Fable 5 --- hooks/lib/template-loader.sh | 7 ++++++- hooks/loop-codex-stop-hook.sh | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/hooks/lib/template-loader.sh b/hooks/lib/template-loader.sh index 13d29f6e..d3886147 100644 --- a/hooks/lib/template-loader.sh +++ b/hooks/lib/template-loader.sh @@ -70,7 +70,12 @@ render_template() { # Scans for {{VAR}} patterns and replaces them with values from environment # Replaced content goes directly to output without re-scanning local awk_exit=0 - content=$(env "${env_vars[@]}" awk ' + # ${arr[@]+"${arr[@]}"} expands to the quoted elements when the array is + # non-empty and to nothing when it is empty -- required because bash 3.2 + # (the macOS default) treats "${empty_array[@]}" as an unbound-variable + # error under `set -u`, which aborts the sourcing hook with a non-zero, + # non-blocking exit and no stderr. + content=$(env ${env_vars[@]+"${env_vars[@]}"} awk ' BEGIN { # Build lookup table from environment variables with TMPL_VAR_ prefix for (name in ENVIRON) { diff --git a/hooks/loop-codex-stop-hook.sh b/hooks/loop-codex-stop-hook.sh index 0c191d4c..2510742f 100755 --- a/hooks/loop-codex-stop-hook.sh +++ b/hooks/loop-codex-stop-hook.sh @@ -1263,7 +1263,7 @@ Provider: codex echo "Running codex review with timeout ${CODEX_TIMEOUT}s in $PROJECT_ROOT (base: $review_base)..." >&2 CODEX_REVIEW_EXIT_CODE=0 - (cd "$PROJECT_ROOT" && run_with_timeout "$CODEX_TIMEOUT" codex review "${CODEX_DISABLE_HOOKS_ARGS[@]}" --base "$review_base" "${CODEX_REVIEW_ARGS[@]}") \ + (cd "$PROJECT_ROOT" && run_with_timeout "$CODEX_TIMEOUT" codex review ${CODEX_DISABLE_HOOKS_ARGS[@]+"${CODEX_DISABLE_HOOKS_ARGS[@]}"} --base "$review_base" "${CODEX_REVIEW_ARGS[@]}") \ > "$CODEX_REVIEW_LOG_FILE" 2>&1 || CODEX_REVIEW_EXIT_CODE=$? echo "Code review exit code: $CODEX_REVIEW_EXIT_CODE" >&2 @@ -1692,7 +1692,7 @@ echo "Codex command saved to: $CODEX_CMD_FILE" >&2 echo "Running summary review with timeout ${CODEX_TIMEOUT}s..." >&2 CODEX_EXIT_CODE=0 -printf '%s' "$CODEX_PROMPT_CONTENT" | run_with_timeout "$CODEX_TIMEOUT" codex exec "${CODEX_DISABLE_HOOKS_ARGS[@]}" "${CODEX_EXEC_ARGS[@]}" - \ +printf '%s' "$CODEX_PROMPT_CONTENT" | run_with_timeout "$CODEX_TIMEOUT" codex exec ${CODEX_DISABLE_HOOKS_ARGS[@]+"${CODEX_DISABLE_HOOKS_ARGS[@]}"} "${CODEX_EXEC_ARGS[@]}" - \ > "$CODEX_STDOUT_FILE" 2> "$CODEX_STDERR_FILE" || CODEX_EXIT_CODE=$? echo "Codex exit code: $CODEX_EXIT_CODE" >&2