Skip to content

fix(housekeep): make orphaned-worktree loop zsh-safe#1714

Merged
carlos-alm merged 2 commits into
mainfrom
fix/housekeep-zsh-orphaned-dirs-loop
Jul 1, 2026
Merged

fix(housekeep): make orphaned-worktree loop zsh-safe#1714
carlos-alm merged 2 commits into
mainfrom
fix/housekeep-zsh-orphaned-dirs-loop

Conversation

@carlos-alm

Copy link
Copy Markdown
Contributor

Summary

  • The Bash tool in Claude Code runs skill bash blocks under zsh, not bash. Unlike bash, zsh does not word-split an unquoted multi-line variable by default — for dir in $ORPHANED_DIRS collapses every orphaned worktree directory into a single loop iteration whose $dir is the whole newline-joined blob the moment more than one orphaned directory exists, breaking the git -C "$dir" lookup with a "no such file or directory" error.
  • Found while running the fleet-wide /housekeep coordinator for real and hitting the identical pattern in its own Phase 3 loop (already fixed there).
  • Fix: iterate with printf '%s\n' "$ORPHANED_DIRS" | while IFS= read -r dir; do ... done, which is portable across bash and zsh.
  • Scoped strictly to this loop. Running the org's own lint-skill.sh/smoke-test-skill.sh against this file surfaced several pre-existing, unrelated findings (missing ## Examples section, missing per-phase Exit condition lines, a few un-commented 2>/dev/null, and the same unquoted <path>/<branch> placeholder syntax issue at two other spots) — none introduced by this change, left out of scope per this repo's own scope-discipline convention. Happy to open a follow-up issue for those if wanted.

Test plan

  • lint-skill.sh/smoke-test-skill.sh: the edited block now passes; remaining findings are pre-existing and unrelated (see above)
  • Reproduced the bug live (multi-entry variable looped with for under zsh collapses to one iteration) and confirmed the new pattern fixes it

Claude Code's Bash tool runs under zsh, which does not word-split an
unquoted multi-line variable the way bash does. 'for dir in $ORPHANED_DIRS'
silently collapsed every orphaned directory into a single iteration
whenever more than one existed, breaking the git -C lookup. Switched to
a printf | while read loop, which is portable across bash and zsh.
@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a real zsh word-splitting bug in the orphaned-worktree loop of SKILL.md: replacing for dir in $ORPHANED_DIRS with printf '%s ' "$ORPHANED_DIRS" | while IFS= read -r dir makes the iteration portable across bash and zsh, where unquoted multi-line variable expansion behaves differently.

  • Core fix (Phase 1 → 1e): the old for loop would collapse all orphaned directories into a single $dir blob under zsh, causing git -C \"$dir\" to fail whenever more than one orphaned worktree existed; the while IFS= read -r pattern processes each path on its own line regardless of shell.
  • Documentation sweep: adds Exit condition footers to every phase, a new ## Examples section, quotes around <path>/<branch> placeholders in example commands, and inline comments explaining every 2>/dev/null suppression.
  • Empty-entry guard ([ -z \"$dir\" ] && continue) handles the trailing newline printf emits when $ORPHANED_DIRS is empty.

Confidence Score: 5/5

Safe to merge — the change is scoped to a single skill file and the fix correctly addresses a real shell portability issue.

The orphaned-worktree loop fix is correct: printf | while IFS= read -r reliably iterates one path per line in both bash and zsh, and the empty-entry guard handles the trailing newline edge case. The rest of the PR is documentation — Exit condition footers, an Examples section, placeholder quoting, and 2>/dev/null annotations — none of which changes runtime behavior. No regressions are introduced.

No files require special attention. The one note worth tracking is that the printf | while pipeline runs the loop body in a subshell, so any future extension that needs to accumulate variables inside the loop will need to switch to process substitution instead.

Important Files Changed

Filename Overview
.claude/skills/housekeep/SKILL.md Fixes the zsh word-splitting bug in the orphaned-worktree loop (for→while-read), adds per-phase Exit conditions, adds an Examples section, quotes placeholder paths in example commands, and documents all 2>/dev/null suppressions. Core fix is correct and the rest are documentation improvements.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["ORPHANED_DIRS\n(newline-separated paths)"] --> B["printf '%s\\n' \"$ORPHANED_DIRS\""]
    B --> C["while IFS= read -r dir"]
    C --> D{"[ -z dir ]?"}
    D -- yes --> C
    D -- no --> E{"dir/.git\nexists?"}
    E -- yes --> F["git -C dir status --short"]
    F --> G{"uncommitted\nchanges?"}
    G -- yes --> H["SKIP dir\n(print changes)"]
    H --> C
    G -- no --> I["ORPHANED (clean): dir\n→ offer rm -rf to user"]
    E -- no --> I
    I --> C
    C -- EOF --> J["done"]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["ORPHANED_DIRS\n(newline-separated paths)"] --> B["printf '%s\\n' \"$ORPHANED_DIRS\""]
    B --> C["while IFS= read -r dir"]
    C --> D{"[ -z dir ]?"}
    D -- yes --> C
    D -- no --> E{"dir/.git\nexists?"}
    E -- yes --> F["git -C dir status --short"]
    F --> G{"uncommitted\nchanges?"}
    G -- yes --> H["SKIP dir\n(print changes)"]
    H --> C
    G -- no --> I["ORPHANED (clean): dir\n→ offer rm -rf to user"]
    E -- no --> I
    I --> C
    C -- EOF --> J["done"]
Loading

Reviews (2): Last reviewed commit: "fix(skills): clean up lint findings in h..." | Re-trigger Greptile

Adds the missing Examples section and per-phase Exit condition lines,
justifies every previously-bare 2>/dev/null suppression, and quotes
the <path>/<branch>/<worktree> placeholders that were silently being
parsed as shell redirection operators instead of literal fill-ins.

No behavior change — these were pre-existing gaps surfaced by running
the org's own lint-skill.sh/smoke-test-skill.sh validators against a
file that had never been run through them.
@carlos-alm carlos-alm merged commit 6f414c4 into main Jul 1, 2026
23 of 24 checks passed
@carlos-alm carlos-alm deleted the fix/housekeep-zsh-orphaned-dirs-loop branch July 1, 2026 22:27
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 1, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant