From a6638d1ef0dd71d2e18607b0f8a34c5a0882d1b0 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Sat, 18 Jul 2026 13:56:36 +0200 Subject: [PATCH 1/5] docs(agents): warn that the harness shell is zsh, which does not word-split MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The top recurring tool-error signature in the agents' own telemetry is `argument required when using the --repo flag` — 41 hits in a 3-day window. It reads like a flaky gh call. It is not. The Bash tool's shell is zsh, which does NOT word-split unquoted expansions. So the bash idiom for iterating "repo number" pairs collapses: for pr in "ksail 6045" …; do set -- $pr → $1='ksail 6045' $2='' → gh pr view '' --repo 'devantler-tech/ksail 6045' The flag is present; the positional argument in front of it vanished, which is why the error misdirects and the real fault stays invisible. Measured, not guessed: pairing every failing tool_result with the command that produced it across 250 sessions (2026-07-14→18) shows 24 of 24 such failures used this idiom and zero used literal arguments. It hits hardest in the per-run trusted-PR sweep, which is first-priority work and where these loops get written most. Documents the trap and three fixes (portable IFS read, zsh's ${=var}, or just writing the calls out), and generalises to every implicit-splitting idiom. RED/GREEN proved in zsh 5.9: the broken form reproduces the empty positional, both replacements recover the fields, and the fixed loop was then run against real gh across three repositories. Co-Authored-By: Claude Opus 4.8 --- AGENTS.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index c19f28c9..6bfff9b3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1115,6 +1115,18 @@ window, unnoticed. The work was never the bottleneck; the **scheduling** was. re-verify **once** — not a fix-one/re-run round trip per finding. - **Parallelize independent setup.** Clones, subagents, and independent investigations start together in the background, not one after another. +- **The Bash tool's shell is `zsh`, which does NOT word-split unquoted expansions — never sweep a + PR list with `set -- $pair`.** The bash idiom for iterating `"repo number"` pairs silently + collapses: `for pr in "ksail 6045" …; do set -- $pr; gh pr view $2 --repo devantler-tech/$1` leaves + `$1` holding the *whole* string and `$2` **empty**, so `gh` runs with no PR number and fails + `argument required when using the --repo flag`. The flag is present — the positional argument in + front of it vanished, which is why the error misdirects. Measured: **24 of 24** such failures + across 250 sessions (2026-07-14→18) used this idiom; **zero** used literal arguments. It is not a + flaky call, it is a shell mismatch, and it hits hardest in the per-run trusted-PR sweep where these + loops are written most. Use one of: + `IFS=' ' read -r repo n <<< "$pr"` (portable), `set -- ${=pr}` (zsh's explicit split flag), or + simply **write the calls out** — two plain `gh` lines beat a clever loop and stay readable. + The same trap applies to every implicit-splitting idiom (`for x in $(cmd)`, `cmd $args`). This changes only *ordering and overlap* — never the quality bar. Validation, RED/GREEN proof, root-cause fixing, and every guardrail are unaffected; the point is to stop paying for them serially. From 1ee9813a65bf24a2752b0cd38f5683200dccb8c3 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Sat, 18 Jul 2026 14:36:10 +0200 Subject: [PATCH 2/5] docs(agents): scope the word-splitting rule to zsh and lead with the portable form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review on #2253 caught a real regression risk in the first version, and verifying its claims confirmed all three empirically. AGENTS.md is the canonical CROSS-TOOL instruction file — Codex, Cursor and Copilot read it too, and the Codex sibling runs bash. The rule was written as though every agent shared Claude Code's zsh Bash tool, which made it actively harmful elsewhere: bash: set -- $pair → $1=ksail $2=6045 (works; nothing to fix) bash: set -- ${=pair} → bad substitution (hard syntax error) zsh: set -- $pair → $1='ksail 6045' $2='' (the actual bug) So a bash-backed agent following the old text would have judged working code broken and replaced it with a syntax error. Now scoped to zsh, leading with the `IFS read` form that is verified correct in both shells, with ${=var} marked zsh-only. Also splits out command substitution, which was wrongly grouped in: zsh DOES IFS-split `$(cmd)` — only parameter expansion is exempt — so an odd result there is ordinary whitespace splitting, a different diagnosis. Grouping them would have sent agents chasing the wrong root cause. Co-Authored-By: Claude Opus 4.8 --- AGENTS.md | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6bfff9b3..454192b9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1115,18 +1115,30 @@ window, unnoticed. The work was never the bottleneck; the **scheduling** was. re-verify **once** — not a fix-one/re-run round trip per finding. - **Parallelize independent setup.** Clones, subagents, and independent investigations start together in the background, not one after another. -- **The Bash tool's shell is `zsh`, which does NOT word-split unquoted expansions — never sweep a - PR list with `set -- $pair`.** The bash idiom for iterating `"repo number"` pairs silently - collapses: `for pr in "ksail 6045" …; do set -- $pr; gh pr view $2 --repo devantler-tech/$1` leaves +- **Splitting a `"repo number"` pair with `set -- $pair` breaks under `zsh` — use the portable + `read` form instead.** Claude Code's Bash tool runs **zsh**, which (unlike bash) does **not** + word-split unquoted *parameter expansions*. So the common bash sweep idiom silently collapses + there: `for pr in "ksail 6045" …; do set -- $pr; gh pr view $2 --repo devantler-tech/$1` leaves `$1` holding the *whole* string and `$2` **empty**, so `gh` runs with no PR number and fails `argument required when using the --repo flag`. The flag is present — the positional argument in front of it vanished, which is why the error misdirects. Measured: **24 of 24** such failures - across 250 sessions (2026-07-14→18) used this idiom; **zero** used literal arguments. It is not a - flaky call, it is a shell mismatch, and it hits hardest in the per-run trusted-PR sweep where these - loops are written most. Use one of: - `IFS=' ' read -r repo n <<< "$pr"` (portable), `set -- ${=pr}` (zsh's explicit split flag), or - simply **write the calls out** — two plain `gh` lines beat a clever loop and stay readable. - The same trap applies to every implicit-splitting idiom (`for x in $(cmd)`, `cmd $args`). + across 250 sessions (2026-07-14→18) used this idiom; **zero** used literal arguments. It hits + hardest in the per-run trusted-PR sweep, where these loops get written most. + **Write it portably and it is correct in every shell:** + ```sh + IFS=' ' read -r repo n <<< "$pr" # works in bash AND zsh + gh pr view "$n" --repo "devantler-tech/$repo" + ``` + Or simply **write the calls out** — two plain `gh` lines beat a clever loop and stay readable. + **Shell-specific notes, so nobody "fixes" working code:** in **bash** `set -- $pair` splits + correctly and needs no change; `set -- ${=pair}` is zsh's explicit-split flag and is a **syntax + error in bash** (`bad substitution`), so never introduce it in a script with a `#!/usr/bin/env bash` + shebang or in the Codex sibling's bash-backed session. When you do not know the active shell, use + the `read` form. + **Not the same hazard:** `for x in $(cmd)` *does* split under zsh (command substitution is still + IFS-split; only parameter expansion is exempt), so an unexpected result there is ordinary + whitespace splitting, not this bug. Keep the two diagnoses apart — the parameter-expansion family + is `set -- $var` and `cmd $args`. This changes only *ordering and overlap* — never the quality bar. Validation, RED/GREEN proof, root-cause fixing, and every guardrail are unaffected; the point is to stop paying for them serially. From 4a8e7e8b78a00802e76d93af7d481765e11be9d0 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Sat, 18 Jul 2026 15:04:25 +0200 Subject: [PATCH 3/5] docs(agents): use a POSIX-portable split form, not a bash/zsh here-string MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review on #2253: the paragraph tells agents to use the read form when the shell is unknown, but here-strings (<<<) are a bash/zsh extension and a syntax error under POSIX /bin/sh. Confirmed: dash reports 'Syntax error: redirection unexpected' before read runs. So the advertised portable fallback would have recreated the cross-shell breakage this rule exists to prevent, one shell over. Leads with pure parameter expansion instead — verified identical output in sh, bash and zsh — and keeps the read form documented as a bash/zsh equivalent with its limit stated. Co-Authored-By: Claude Opus 4.8 --- AGENTS.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 454192b9..f53acea1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1126,9 +1126,13 @@ window, unnoticed. The work was never the bottleneck; the **scheduling** was. hardest in the per-run trusted-PR sweep, where these loops get written most. **Write it portably and it is correct in every shell:** ```sh - IFS=' ' read -r repo n <<< "$pr" # works in bash AND zsh + repo=${pr%% *}; n=${pr##* } # POSIX parameter expansion: sh, bash AND zsh gh pr view "$n" --repo "devantler-tech/$repo" ``` + (`IFS=' ' read -r repo n <<< "$pr"` is equivalent **in bash and zsh only** — the here-string `<<<` + is a bash/zsh extension and a **syntax error** under POSIX `/bin/sh`, e.g. dash. The parameter- + expansion form above has no such limit, so prefer it when the shell is unknown or the file carries + a `#!/bin/sh` shebang.) Or simply **write the calls out** — two plain `gh` lines beat a clever loop and stay readable. **Shell-specific notes, so nobody "fixes" working code:** in **bash** `set -- $pair` splits correctly and needs no change; `set -- ${=pair}` is zsh's explicit-split flag and is a **syntax From 379fd313234cc5c9ec5f0f78d47d102cb0353a2c Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Sat, 18 Jul 2026 15:23:11 +0200 Subject: [PATCH 4/5] docs(agents): remove the contradictory 'use the read form' sentence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit replaced the code block with POSIX parameter expansion but left a trailing sentence still directing agents to the read form when the shell is unknown — the exact case where its bash/zsh-only here-string breaks. So the paragraph contradicted itself, and an agent reading to the end would land on the wrong advice. Points to the parameter-expansion form instead: the only one of the three with no shell restriction. Co-Authored-By: Claude Opus 4.8 --- AGENTS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f53acea1..d641e924 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1137,8 +1137,8 @@ window, unnoticed. The work was never the bottleneck; the **scheduling** was. **Shell-specific notes, so nobody "fixes" working code:** in **bash** `set -- $pair` splits correctly and needs no change; `set -- ${=pair}` is zsh's explicit-split flag and is a **syntax error in bash** (`bad substitution`), so never introduce it in a script with a `#!/usr/bin/env bash` - shebang or in the Codex sibling's bash-backed session. When you do not know the active shell, use - the `read` form. + shebang or in the Codex sibling's bash-backed session. **When you do not know the active shell, use + the parameter-expansion form above** — it is the only one of the three with no shell restriction. **Not the same hazard:** `for x in $(cmd)` *does* split under zsh (command substitution is still IFS-split; only parameter expansion is exempt), so an unexpected result there is ordinary whitespace splitting, not this bug. Keep the two diagnoses apart — the parameter-expansion family From 434ca5ffdac6b8cd4cceb8221b42bd913ea21c21 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Sat, 18 Jul 2026 16:26:36 +0200 Subject: [PATCH 5/5] docs(agents): fix the summary heading to name the POSIX form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bold rule — the line an agent reads as the summary — still said 'use the portable read form', while the body two lines down explains that form's here-string is bash/zsh-only and invalid under /bin/sh. Anyone skimming got the shell-restricted answer as the headline. Names the parameter-expansion form instead, matching the example and the body. Co-Authored-By: Claude Opus 4.8 --- AGENTS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d641e924..2857d2a1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1115,8 +1115,8 @@ window, unnoticed. The work was never the bottleneck; the **scheduling** was. re-verify **once** — not a fix-one/re-run round trip per finding. - **Parallelize independent setup.** Clones, subagents, and independent investigations start together in the background, not one after another. -- **Splitting a `"repo number"` pair with `set -- $pair` breaks under `zsh` — use the portable - `read` form instead.** Claude Code's Bash tool runs **zsh**, which (unlike bash) does **not** +- **Splitting a `"repo number"` pair with `set -- $pair` breaks under `zsh` — use the POSIX + parameter-expansion form instead.** Claude Code's Bash tool runs **zsh**, which (unlike bash) does **not** word-split unquoted *parameter expansions*. So the common bash sweep idiom silently collapses there: `for pr in "ksail 6045" …; do set -- $pr; gh pr view $2 --repo devantler-tech/$1` leaves `$1` holding the *whole* string and `$2` **empty**, so `gh` runs with no PR number and fails