Skip to content

codex-implementer: timeout guard is a no-op under zsh (exit 127 before codex runs) #13

Description

@sherySJ

Summary

The codex-implementer agent's timeout guard is a no-op under zsh. The command dies with exit 127 before codex exec ever starts, so the advertised ten-minute cap silently never applies. Since zsh is the macOS default shell and the surrounding comment specifically targets macOS, this likely affects most users on the platform.

Location

agents/codex-implementer.md, section "How you run codex", step 2 (v4.0.0, lines 73-76):

T=$(command -v gtimeout || command -v timeout || true)
[ -z "$T" ] && echo "WARN: no timeout binary - codex runs uncapped (brew install coreutils to cap)"

${T:+$T 600} codex exec \

The flag-discipline table at line 94 documents this as "Ten-minute wall clock when timeout/gtimeout exists".

Cause

bash performs word splitting on the unquoted ${T:+$T 600} expansion, producing two arguments. zsh does not split unquoted parameter expansions, so the whole thing becomes a single word: a filename literally named /opt/homebrew/bin/gtimeout 600, which does not exist.

Reproduction

$ zsh -c 'T=$(command -v gtimeout || true); ${T:+$T 2} echo hello'
zsh:1: no such file or directory: /opt/homebrew/bin/gtimeout 2

$ bash -c 'T=$(command -v gtimeout || true); ${T:+$T 2} echo hello'
hello

Impact

Two failure modes, and the second is the concerning one:

  1. On a machine with coreutils installed, the lane's first codex exec attempt fails outright with exit 127. Observed live on 2026-08-07: the agent caught the empty result and retried without the wrapper, so the task still completed, but the retry runs uncapped.
  2. On a machine without coreutils, T is empty, the expansion vanishes, and codex runs uncapped. That path is intended and warned about, so it is fine.

The net effect is that the cap never actually applies under zsh either way, while the documentation says it does. A hung codex run has no wall-clock bound and never returns STATUS: timeout.

Suggested fix

Positional parameters expand correctly in both shells and handle the empty case cleanly:

T=$(command -v gtimeout || command -v timeout || true)
[ -z "$T" ] && echo "WARN: no timeout binary - codex runs uncapped (brew install coreutils to cap)"

if [ -n "$T" ]; then set -- "$T" 600; else set --; fi

"$@" codex exec \
  --model gpt-5.6-luna \
  -c model_reasoning_effort=max \
  ...

Verified four ways, bash and zsh, each with and without the binary present, all correct. Confirmed it actually caps:

$ zsh -c 'T=$(command -v gtimeout || true); if [ -n "$T" ]; then set -- "$T" 2; else set --; fi; "$@" sleep 10; echo "exit=$?"'
exit=124

An alternative is ${=T} to force splitting, but that is zsh-only and would break bash, so the positional-parameter form seems preferable for a cross-shell agent.

Environment

  • Plugin fable-advisor@fable-advisor v4.0.0
  • macOS (Darwin 27.0.0), Apple silicon, zsh as the default shell
  • Codex CLI 0.146.0, coreutils installed via Homebrew (gtimeout at /opt/homebrew/bin/gtimeout)

Thanks for the plugin. The architect-as-orchestrator doctrine has been working well for us otherwise; this was the only defect that surfaced across a full day of use.

Activity

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