Skip to content

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

Closed

Description

@marcosdetry

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=1

Environment: 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 _ts

Verified after the patch:

  • bash -n and zsh -n both pass.
  • Under both bash and zsh: _gstack_codex_select_model exec|review resolve the model, _gstack_codex_auth_probe passes, _gstack_codex_model_probe returns MODEL_OK, and the /ship preflight block prints CODEX_MODE: ready from 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: two outside-voice-invocation tests 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

  1. akshaykalia commented on Oct 4, 2026

    @akshaykalia
    Contributor

    Still reproduces on v1.91.18.0 (macOS, zsh 5.9, Claude Code Bash tool), same /../scripts/resolve-codex-generation-model.ts error from /plan-eng-review and /office-hours. Under bash -c the same probe selects the configured model fine.

    One correction to the scope above: bin/gstack-egress-lib.sh is 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:

    # bin/gstack-egress-lib.sh:41-44
    case "${BASH_SOURCE[0]}" in
      */*) _gstack_egress_lib_dir="$(cd "${BASH_SOURCE[0]%/*}" && pwd)" ;;
      *) _gstack_egress_lib_dir="$(pwd)" ;;
    esac
    cd ~/some/other/repo
    zsh -c '. ~/.claude/skills/gstack/bin/gstack-egress-lib.sh; echo "$_gstack_egress_lib_dir"'
    # prints ~/some/other/repo, not ~/.claude/skills/gstack/bin

    So under zsh _gstack_egress_home looks for gstack-state-root.sh and gstack_egress_receipt uses gstack-egress-receipt in 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 (bash BASH_SOURCE, zsh functions_source / ${(%):-%x}) would fix this one too.

  2. openiteam commented on Oct 4, 2026

    @openiteam

    Still reproduces on v1.91.19.0 (macOS 27, zsh, Claude Code Bash tool, codex-cli 0.160.0): /plan-eng-review outside voice reports MODEL_UNUSABLE with the same /../scripts/resolve-codex-generation-model.ts path, while bash -c selects the configured model fine. A local patch that resolves the probe's directory once at source time (bash BASH_SOURCE, zsh ${(%):-%x} via eval, fallback to the default install path) fixes all five call sites and keeps test/codex-hardening.test.ts green (68/68). Looking forward to #3027 landing.

  3. capy-ai commented on Oct 6, 2026

    @capy-ai
    Contributor

    Fixed in v1.91.32.0 (#3057). Sourced helpers locate themselves under bash and zsh. Any other shell gets CODEX_MODE: helper_unavailable with a fix link instead of model_unusable. Check: zsh -c 'source ~/.claude/skills/gstack/bin/gstack-codex-probe && _gstack_codex_select_model exec' should print CODEX_MODEL: <model> (exec; source: ...). Thanks @marcosdetry and @akshaykalia for the diagnosis.

    Upgrade: Claude Code /gstack-upgrade; Codex and other hosts cd <source from ./setup --status> && git pull && ./setup --host <host>. If it still reproduces, reopen with your ./setup --status output.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions