Repository navigation
gstack-codex-probe: Codex outside review always unavailable under zsh since v1.91.16.0 (BASH_SOURCE empty → "/../scripts/resolve-codex-generation-model.ts") #3024
Description
Summary
Since v1.91.16.0 (ac20ef1, #3014) every Codex outside pass fails when the skill's bash block is sourced by zsh, which is the default shell on macOS (and what Claude Code's Bash tool runs here). /ship, /review, /codex and the design-lite voice all report Codex as unavailable even though Codex is installed, authenticated and the model works.
error: Module not found "/../scripts/resolve-codex-generation-model.ts"
CODEX_MODEL: invalid — Codex outside review unavailable; missing coverage. No Codex call was made. Fix the model choice above; gstack never substitutes its default for it.
MODEL_UNUSABLE
CODEX_MODE: model_unusable
Cause
bin/gstack-codex-probe is sourced from the skill templates (14 call sites), so it runs in whatever shell the host uses. #3014 changed _gstack_codex_select_model to locate the resolver with:
bun run "${BASH_SOURCE[0]%/*}/../scripts/resolve-codex-generation-model.ts" ...BASH_SOURCE only exists in bash. Under zsh it is empty, so the path becomes /../scripts/resolve-codex-generation-model.ts. The resolver call fails, the selection is rejected, and _gstack_codex_model_probe returns "model unusable". The same pattern also appears in _gstack_codex_model_probe (state root for the probe cache) and _gstack_codex_log_event (analytics root); there it fails silently (no cache, no event) instead of loudly.
The other BASH_SOURCE users in bin/ (gstack-paths, gstack-egress-lib.sh) run as bash scripts, so they are fine. Only the sourced probe is affected.
Repro
bash -c 'source ~/.claude/skills/gstack/bin/gstack-codex-probe && _gstack_codex_select_model exec; echo rc=$?'
# CODEX_MODEL: <model> (exec; source: gstack default ...) rc=0
zsh -c 'source ~/.claude/skills/gstack/bin/gstack-codex-probe && _gstack_codex_select_model exec; echo rc=$?'
# error: Module not found "/../scripts/resolve-codex-generation-model.ts" rc=1Environment: macOS, zsh 5.9, gstack v1.91.16.0 (latest at the time of writing), codex-cli 0.154.0, bun on PATH.
Fix (tested locally)
A small helper that returns the probe's own directory in both shells (bash: BASH_SOURCE; zsh: functions_source, wrapped in eval so bash never parses the zsh-only expansion; fallback to the default install path), used in all four places:
diff --git a/bin/gstack-codex-probe b/bin/gstack-codex-probe
index d840c20c..47d6268c 100755
--- a/bin/gstack-codex-probe
+++ b/bin/gstack-codex-probe
@@ -3,6 +3,7 @@
# Sourced from template bash blocks; never execute directly.
#
# Functions (all prefixed with _gstack_codex_ for namespace hygiene):
+# _gstack_codex_bin_dir — this file's directory under bash or zsh
# _gstack_codex_auth_probe — multi-signal auth check (env + file)
# _gstack_codex_select_model — resolve + print the runtime model (#2914)
# _gstack_codex_model_probe — round-trip probe of the selected model (#2477)
@@ -16,6 +17,22 @@
# - All functions prefix with _gstack_codex_.
# - No command execution at source time (only function defs).
+# --- Own directory (bash and zsh) ------------------------------------------
+# Template blocks are sourced by whatever shell the host runs, often zsh.
+# BASH_SOURCE is bash-only: under zsh "${BASH_SOURCE[0]%/*}" is empty and the
+# resolver path became "/../scripts/...", so every Codex pass reported
+# "CODEX_MODEL: invalid". zsh's functions_source names the defining file.
+_gstack_codex_bin_dir() {
+ local _d=""
+ if [ -n "${BASH_SOURCE[0]:-}" ]; then
+ _d="${BASH_SOURCE[0]%/*}"
+ elif [ -n "${ZSH_VERSION:-}" ]; then
+ eval '_d="${functions_source[_gstack_codex_bin_dir]%/*}"'
+ fi
+ [ -n "$_d" ] || _d="$HOME/.claude/skills/gstack/bin"
+ printf '%s\n' "$_d"
+}
+
# --- Auth probe -------------------------------------------------------------
_gstack_codex_auth_probe() {
@@ -49,9 +66,9 @@ _gstack_codex_select_model() {
return 1
fi
if [ -n "$_explicit" ]; then
- _line=$(bun run "${BASH_SOURCE[0]%/*}/../scripts/resolve-codex-generation-model.ts" --runtime "$_kind" --explicit "$_explicit")
+ _line=$(bun run "$(_gstack_codex_bin_dir)/../scripts/resolve-codex-generation-model.ts" --runtime "$_kind" --explicit "$_explicit")
else
- _line=$(bun run "${BASH_SOURCE[0]%/*}/../scripts/resolve-codex-generation-model.ts" --runtime "$_kind")
+ _line=$(bun run "$(_gstack_codex_bin_dir)/../scripts/resolve-codex-generation-model.ts" --runtime "$_kind")
fi
_rc=$?
_GSTACK_CODEX_SEL="${_line%%$'\t'*}"
@@ -108,7 +125,7 @@ _gstack_codex_model_probe() {
# State root from the shared twin (sourced in a subshell, so the caller's
# shell is untouched). A missing twin only disables the cache.
local _gstack_home _cache=""
- _gstack_home="$(. "${BASH_SOURCE[0]%/*}/gstack-state-root.sh" 2>/dev/null && gstack_state_root)" || _gstack_home=""
+ _gstack_home="$(. "$(_gstack_codex_bin_dir)/gstack-state-root.sh" 2>/dev/null && gstack_state_root)" || _gstack_home=""
[ -n "$_gstack_home" ] && _cache="$_gstack_home/.codex-model-probe"
# Cache signature (#2787): everything that can change the round trip's
# outcome without the hour passing — the model, the Codex home and its
@@ -295,7 +312,7 @@ _gstack_codex_log_event() {
local _duration="${2:-0}"
[ "${_TEL:-off}" = "off" ] && return 0
local _root
- _root="$(. "${BASH_SOURCE[0]%/*}/gstack-state-root.sh" 2>/dev/null && gstack_state_root)" || return 0
+ _root="$(. "$(_gstack_codex_bin_dir)/gstack-state-root.sh" 2>/dev/null && gstack_state_root)" || return 0
[ -n "$_root" ] || return 0
mkdir -p "$_root/analytics" 2>/dev/null || return 0
local _tsVerified after the patch:
bash -nandzsh -nboth pass.- Under both bash and zsh:
_gstack_codex_select_model exec|reviewresolve the model,_gstack_codex_auth_probepasses,_gstack_codex_model_probereturnsMODEL_OK, and the/shippreflight block printsCODEX_MODE: readyfrom zsh. bun test test/codex-hardening.test.ts test/codex-model-probe.test.ts test/codex-web-search-flag.test.ts test/codex-skill-isolation.test.ts test/outside-voice-invocation.test.ts: 130 pass / 3 fail both before and after the patch. The 3 failures are unrelated: twooutside-voice-invocationtests compare/var/...with/private/var/...on macOS, and the bash-native watchdog test hits its 5 s timeout.
A regression test that sources the probe from zsh -c (skipped when zsh is absent) would catch this class, since the existing tests only exercise bash.
Activity
akshaykalia commentedon Oct 4, 2026
Still reproduces on v1.91.18.0 (macOS, zsh 5.9, Claude Code Bash tool), same
/../scripts/resolve-codex-generation-model.tserror from/plan-eng-reviewand/office-hours. Underbash -cthe same probe selects the configured model fine.One correction to the scope above:
bin/gstack-egress-lib.shis not run as a bash script. Its header says it's sourced, and skill templates source it for_aside_exec. Under zsh its self-locate falls through to the caller's working directory:So under zsh
_gstack_egress_homelooks forgstack-state-root.shandgstack_egress_receiptusesgstack-egress-receiptin the repo the user is working in. That fails silently (missing receipts / state-root placeholder), or would source a same-named file from that repo if one existed. The_gstack_codex_bin_dir-style helper from the proposed fix (bashBASH_SOURCE, zshfunctions_source/${(%):-%x}) would fix this one too.openiteam commentedon Oct 4, 2026
Still reproduces on v1.91.19.0 (macOS 27, zsh, Claude Code Bash tool, codex-cli 0.160.0):
/plan-eng-reviewoutside voice reportsMODEL_UNUSABLEwith the same/../scripts/resolve-codex-generation-model.tspath, whilebash -cselects the configured model fine. A local patch that resolves the probe's directory once at source time (bashBASH_SOURCE, zsh${(%):-%x}viaeval, fallback to the default install path) fixes all five call sites and keepstest/codex-hardening.test.tsgreen (68/68). Looking forward to #3027 landing.v1.91.32.0 fix: Codex works from zsh, free text never enters shell co…
capy-ai commentedon Oct 6, 2026
Fixed in v1.91.32.0 (#3057). Sourced helpers locate themselves under bash and zsh. Any other shell gets
CODEX_MODE: helper_unavailablewith a fix link instead ofmodel_unusable. Check:zsh -c 'source ~/.claude/skills/gstack/bin/gstack-codex-probe && _gstack_codex_select_model exec'should printCODEX_MODEL: <model> (exec; source: ...). Thanks @marcosdetry and @akshaykalia for the diagnosis.Upgrade: Claude Code
/gstack-upgrade; Codex and other hostscd <source from ./setup --status> && git pull && ./setup --host <host>. If it still reproduces, reopen with your./setup --statusoutput.v1.91.32.0 fix: Codex works from zsh, free text never enters shell co…