Skip to content

fix(codex-implementer): make the timeout wrapper work under zsh - #12

Open
wooter wants to merge 1 commit into
DannyMac180:mainfrom
wooter:fix/zsh-timeout-wrapper
Open

fix(codex-implementer): make the timeout wrapper work under zsh#12
wooter wants to merge 1 commit into
DannyMac180:mainfrom
wooter:fix/zsh-timeout-wrapper

Conversation

@wooter

@wooter wooter commented Aug 6, 2026

Copy link
Copy Markdown

The bug

agents/codex-implementer.md tells the lane to invoke codex as:

T=$(command -v gtimeout || command -v timeout || true)
${T:+$T 600} codex exec \

${T:+$T 600} depends on bash word-splitting an unquoted parameter expansion. zsh doesn't do that by default, so "$T 600" arrives at execve as one argv word and the run dies immediately:

no such file or directory: /opt/homebrew/bin/gtimeout 600

Claude Code's Bash tool runs zsh on macOS, which is where this lane predominantly runs — so on those machines the timeout wrapper fails every single invocation. I logged this from eight separate sessions (across Sonnet and Fable lanes) before tracing it to this one line; each time the lane worked around it ad hoc, which is exactly the kind of drift the flag-discipline section exists to prevent.

The fix

Build the prefix as positional parameters:

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

"$@" codex exec \

"$@" expands to zero words when no positional parameters are set — in bash and zsh — so the uncapped fallback needs no special-casing and the wrapped path gets two clean argv words.

Secondary hardening: probe by running each candidate instead of trusting command -v. A command -v hit isn't proof the binary executes — a stale shell hash cache or a symlink left dangling by a relinked coreutils both yield exec failures, and I saw both reported against this preflight.

Verification

Shell Result
bash picks gtimeout, runs the wrapped command
zsh picks gtimeout, runs the wrapped command
sh picks gtimeout, runs the wrapped command
zsh, no timeout binary on PATH warns once, runs uncapped

Docs-only change, scoped to the one code block and its row in the flag-discipline table. No behavioural change on bash.

`${T:+$T 600} codex exec ...` relies on bash word-splitting an unquoted
parameter expansion. zsh does not word-split unquoted expansions, so
"$T 600" reaches execve as a single argv word and the run dies with:

    no such file or directory: /opt/homebrew/bin/gtimeout 600

The Claude Code Bash tool runs zsh on macOS, which is where this lane
mostly lives, so the timeout wrapper fails there every time. I hit it in
eight separate sessions before tracking it to this line.

Build the prefix as positional parameters instead. `"$@"` expands to zero
words when nothing is set, in both bash and zsh, so the uncapped fallback
still works with no special-casing.

Also probe by *running* each candidate rather than trusting `command -v`:
a hit is not proof the binary executes (a stale shell hash cache, or a
symlink left dangling by a relinked coreutils, both produce exec 127).

Verified: bash, zsh and sh all pick gtimeout and run the wrapped command;
with no timeout binary on PATH all three warn once and run uncapped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@wooter
wooter force-pushed the fix/zsh-timeout-wrapper branch from 39c8659 to ba36e25 Compare August 10, 2026 11:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant