diff --git a/bin/gstack-codex-probe b/bin/gstack-codex-probe
index 940dacf84..39893ef6a 100755
--- a/bin/gstack-codex-probe
+++ b/bin/gstack-codex-probe
@@ -5,7 +5,7 @@
# Functions (all prefixed with _gstack_codex_ for namespace hygiene):
# _gstack_codex_auth_probe — multi-signal auth check (env + file)
# _gstack_codex_version_check — warn on known-bad Codex CLI versions
-# _gstack_codex_timeout_wrapper — gtimeout -> timeout -> unwrapped fallback
+# _gstack_codex_timeout_wrapper — gtimeout -> timeout -> shell-watchdog fallback
# _gstack_codex_log_event — telemetry emission to ~/.gstack/analytics/
#
# Hygiene rules (enforced by test/codex-hardening.test.ts):
@@ -53,17 +53,50 @@ _gstack_codex_version_check() {
_gstack_codex_timeout_wrapper() {
# Resolve wrapper binary: prefer gtimeout (Homebrew coreutils on macOS),
- # fall back to timeout (Linux), else run unwrapped. Arguments: $1 is the
- # duration in seconds; rest is the command to run.
+ # fall back to timeout (Linux), else a pure-shell watchdog so hang
+ # protection still works on machines with neither (e.g. bare macOS).
+ # Arguments: $1 is the duration in seconds; rest is the command to run.
local _duration="$1"
shift
local _to
_to=$(command -v gtimeout 2>/dev/null || command -v timeout 2>/dev/null || echo "")
if [ -n "$_to" ]; then
"$_to" "$_duration" "$@"
- else
- "$@"
+ return $?
fi
+ # Shell watchdog: background the command, poll every 5s, TERM at the
+ # deadline (KILL 10s later if TERM is ignored), and report exit 124 like
+ # GNU timeout so callers' hang handling still fires. The brace group
+ # routes the shell's own stderr to /dev/null so bash's "Terminated" job
+ # notices never leak into skill output, while fd 3 carries the real
+ # stderr through to the command (call-site 2> captures keep working).
+ # Both jobs are reaped inside the silenced group so no notice surfaces
+ # later and nothing outlives the call holding the caller's pipes open.
+ local _cmd_pid _watchdog_pid
+ local _rc=0
+ {
+ "$@" 2>&3 3>&- &
+ _cmd_pid=$!
+ (
+ _waited=0
+ while [ "$_waited" -lt "$_duration" ]; do
+ sleep 5
+ kill -0 "$_cmd_pid" 2>/dev/null || exit 0
+ _waited=$((_waited + 5))
+ done
+ kill -TERM "$_cmd_pid" 2>/dev/null
+ sleep 10
+ kill -KILL "$_cmd_pid" 2>/dev/null
+ ) >/dev/null 2>&1 3>&- &
+ _watchdog_pid=$!
+ wait "$_cmd_pid" || _rc=$?
+ kill "$_watchdog_pid" 2>/dev/null
+ wait "$_watchdog_pid" 2>/dev/null || :
+ } 3>&2 2>/dev/null
+ if [ "$_rc" -eq 143 ] || [ "$_rc" -eq 137 ]; then
+ _rc=124
+ fi
+ return "$_rc"
}
# --- Telemetry event --------------------------------------------------------
diff --git a/codex/SKILL.md b/codex/SKILL.md
index 33228ff9b..574cdde57 100644
--- a/codex/SKILL.md
+++ b/codex/SKILL.md
@@ -986,7 +986,7 @@ cd "$_REPO_ROOT"
# only fires if Bash's own timeout doesn't.
_gstack_codex_timeout_wrapper 330 codex review "IMPORTANT: Do NOT read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or agents/. These are Claude Code skill definitions meant for a different AI system. Do NOT modify agents/openai.yaml. Stay focused on repository code only.
-Review the changes on this branch against the base branch . Run git diff origin/...HEAD 2>/dev/null || git diff ...HEAD to see the diff and review only those changes." -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR"
+Review the changes on this branch against the base branch . Run git diff origin/...HEAD 2>/dev/null || git diff ...HEAD to see the diff and review only those changes." -c 'model_reasoning_effort="high"' -c 'web_search="cached"' < /dev/null 2>"$TMPERR"
_CODEX_EXIT=$?
if [ "$_CODEX_EXIT" = "124" ]; then
_gstack_codex_log_event "codex_timeout" "330"
@@ -1024,7 +1024,7 @@ _PROMPT_FILE=$(mktemp "$TMP_ROOT/codex-prompt-XXXXXX.txt")
git diff "...HEAD" 2>/dev/null
printf '\nDIFF_END\n'
} > "$_PROMPT_FILE"
-_gstack_codex_timeout_wrapper 330 codex exec -s read-only "$(cat "$_PROMPT_FILE")" -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR"
+_gstack_codex_timeout_wrapper 330 codex exec -s read-only "$(cat "$_PROMPT_FILE")" -c 'model_reasoning_effort="high"' -c 'web_search="cached"' < /dev/null 2>"$TMPERR"
_CODEX_EXIT=$?
rm -f "$_PROMPT_FILE"
if [ "$_CODEX_EXIT" = "124" ]; then
@@ -1267,7 +1267,7 @@ fi
# Fix 1+2: wrap with timeout (gtimeout/timeout fallback chain via probe helper),
# capture stderr to $TMPERR for auth error detection (was: 2>/dev/null).
TMPERR=${TMPERR:-$(mktemp "$TMP_ROOT/codex-err-XXXXXX.txt")}
-_gstack_codex_timeout_wrapper 600 codex exec "" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' --enable web_search_cached --json < /dev/null 2>"$TMPERR" | PYTHONUNBUFFERED=1 "$PYTHON_CMD" -u -c "
+_gstack_codex_timeout_wrapper 600 codex exec "" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json < /dev/null 2>"$TMPERR" | PYTHONUNBUFFERED=1 "$PYTHON_CMD" -u -c "
import sys, json
turn_completed_count = 0
for line in sys.stdin:
@@ -1422,7 +1422,7 @@ if [ -z "$PYTHON_CMD" ]; then
exit 1
fi
# Fix 1: wrap with timeout (gtimeout/timeout fallback chain via probe helper)
-_gstack_codex_timeout_wrapper 600 codex exec "" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="medium"' --enable web_search_cached --json < /dev/null 2>"$TMPERR" | PYTHONUNBUFFERED=1 "$PYTHON_CMD" -u -c "
+_gstack_codex_timeout_wrapper 600 codex exec "" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="medium"' -c 'web_search="cached"' --json < /dev/null 2>"$TMPERR" | PYTHONUNBUFFERED=1 "$PYTHON_CMD" -u -c "
import sys, json
for line in sys.stdin:
line = line.strip()
@@ -1476,7 +1476,7 @@ if [ -z "$PYTHON_CMD" ]; then
fi
cd "$_REPO_ROOT" || exit 1
# Fix 1: wrap with timeout (gtimeout/timeout fallback chain via probe helper)
-_gstack_codex_timeout_wrapper 600 codex exec resume "" -c 'sandbox_mode="read-only"' -c 'model_reasoning_effort="medium"' --enable web_search_cached --json < /dev/null 2>"$TMPERR" | PYTHONUNBUFFERED=1 "$PYTHON_CMD" -u -c "
+_gstack_codex_timeout_wrapper 600 codex exec resume "" -c 'sandbox_mode="read-only"' -c 'model_reasoning_effort="medium"' -c 'web_search="cached"' --json < /dev/null 2>"$TMPERR" | PYTHONUNBUFFERED=1 "$PYTHON_CMD" -u -c "
"
# Fix 1: same hang detection pattern as new-session block
@@ -1548,8 +1548,10 @@ uses them. If the user wants a specific model, pass `-m` through to codex.
tasks (OpenAI issues #8545, #8402, #6931). Users can override with `--xhigh` flag
(e.g., `/codex review --xhigh`) when they want maximum reasoning and are willing to wait.
-**Web search:** All codex commands use `--enable web_search_cached` so Codex can look up
-docs and APIs during review. This is OpenAI's cached index — fast, no extra cost.
+**Web search:** All codex commands pass `-c 'web_search="cached"'` so Codex can look up
+docs and APIs during review. This is OpenAI's cached index — fast, no extra cost. (Codex
+CLI ≥ 0.144 deprecated the older `--enable web_search_cached` flag; the `-c` override
+pins cached mode now that web search is on by default.)
If the user specifies a model (e.g., `/codex review -m gpt-5.1-codex-max`
or `/codex challenge -m gpt-5.2`), pass the `-m` flag through to codex.
diff --git a/codex/SKILL.md.tmpl b/codex/SKILL.md.tmpl
index 333de7d8d..cc5aaac82 100644
--- a/codex/SKILL.md.tmpl
+++ b/codex/SKILL.md.tmpl
@@ -177,7 +177,7 @@ cd "$_REPO_ROOT"
# only fires if Bash's own timeout doesn't.
_gstack_codex_timeout_wrapper 330 codex review "IMPORTANT: Do NOT read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or agents/. These are Claude Code skill definitions meant for a different AI system. Do NOT modify agents/openai.yaml. Stay focused on repository code only.
-Review the changes on this branch against the base branch . Run git diff origin/...HEAD 2>/dev/null || git diff ...HEAD to see the diff and review only those changes." -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR"
+Review the changes on this branch against the base branch . Run git diff origin/...HEAD 2>/dev/null || git diff ...HEAD to see the diff and review only those changes." -c 'model_reasoning_effort="high"' -c 'web_search="cached"' < /dev/null 2>"$TMPERR"
_CODEX_EXIT=$?
if [ "$_CODEX_EXIT" = "124" ]; then
_gstack_codex_log_event "codex_timeout" "330"
@@ -215,7 +215,7 @@ _PROMPT_FILE=$(mktemp "$TMP_ROOT/codex-prompt-XXXXXX.txt")
git diff "...HEAD" 2>/dev/null
printf '\nDIFF_END\n'
} > "$_PROMPT_FILE"
-_gstack_codex_timeout_wrapper 330 codex exec -s read-only "$(cat "$_PROMPT_FILE")" -c 'model_reasoning_effort="high"' --enable web_search_cached < /dev/null 2>"$TMPERR"
+_gstack_codex_timeout_wrapper 330 codex exec -s read-only "$(cat "$_PROMPT_FILE")" -c 'model_reasoning_effort="high"' -c 'web_search="cached"' < /dev/null 2>"$TMPERR"
_CODEX_EXIT=$?
rm -f "$_PROMPT_FILE"
if [ "$_CODEX_EXIT" = "124" ]; then
@@ -336,7 +336,7 @@ fi
# Fix 1+2: wrap with timeout (gtimeout/timeout fallback chain via probe helper),
# capture stderr to $TMPERR for auth error detection (was: 2>/dev/null).
TMPERR=${TMPERR:-$(mktemp "$TMP_ROOT/codex-err-XXXXXX.txt")}
-_gstack_codex_timeout_wrapper 600 codex exec "" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' --enable web_search_cached --json < /dev/null 2>"$TMPERR" | PYTHONUNBUFFERED=1 "$PYTHON_CMD" -u -c "
+_gstack_codex_timeout_wrapper 600 codex exec "" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="high"' -c 'web_search="cached"' --json < /dev/null 2>"$TMPERR" | PYTHONUNBUFFERED=1 "$PYTHON_CMD" -u -c "
import sys, json
turn_completed_count = 0
for line in sys.stdin:
@@ -491,7 +491,7 @@ if [ -z "$PYTHON_CMD" ]; then
exit 1
fi
# Fix 1: wrap with timeout (gtimeout/timeout fallback chain via probe helper)
-_gstack_codex_timeout_wrapper 600 codex exec "" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="medium"' --enable web_search_cached --json < /dev/null 2>"$TMPERR" | PYTHONUNBUFFERED=1 "$PYTHON_CMD" -u -c "
+_gstack_codex_timeout_wrapper 600 codex exec "" -C "$_REPO_ROOT" -s read-only -c 'model_reasoning_effort="medium"' -c 'web_search="cached"' --json < /dev/null 2>"$TMPERR" | PYTHONUNBUFFERED=1 "$PYTHON_CMD" -u -c "
import sys, json
for line in sys.stdin:
line = line.strip()
@@ -545,7 +545,7 @@ if [ -z "$PYTHON_CMD" ]; then
fi
cd "$_REPO_ROOT" || exit 1
# Fix 1: wrap with timeout (gtimeout/timeout fallback chain via probe helper)
-_gstack_codex_timeout_wrapper 600 codex exec resume "" -c 'sandbox_mode="read-only"' -c 'model_reasoning_effort="medium"' --enable web_search_cached --json < /dev/null 2>"$TMPERR" | PYTHONUNBUFFERED=1 "$PYTHON_CMD" -u -c "
+_gstack_codex_timeout_wrapper 600 codex exec resume "" -c 'sandbox_mode="read-only"' -c 'model_reasoning_effort="medium"' -c 'web_search="cached"' --json < /dev/null 2>"$TMPERR" | PYTHONUNBUFFERED=1 "$PYTHON_CMD" -u -c "
"
# Fix 1: same hang detection pattern as new-session block
@@ -617,8 +617,10 @@ uses them. If the user wants a specific model, pass `-m` through to codex.
tasks (OpenAI issues #8545, #8402, #6931). Users can override with `--xhigh` flag
(e.g., `/codex review --xhigh`) when they want maximum reasoning and are willing to wait.
-**Web search:** All codex commands use `--enable web_search_cached` so Codex can look up
-docs and APIs during review. This is OpenAI's cached index — fast, no extra cost.
+**Web search:** All codex commands pass `-c 'web_search="cached"'` so Codex can look up
+docs and APIs during review. This is OpenAI's cached index — fast, no extra cost. (Codex
+CLI ≥ 0.144 deprecated the older `--enable web_search_cached` flag; the `-c` override
+pins cached mode now that web search is on by default.)
If the user specifies a model (e.g., `/codex review -m gpt-5.1-codex-max`
or `/codex challenge -m gpt-5.2`), pass the `-m` flag through to codex.