fix(housekeep): make orphaned-worktree loop zsh-safe#1714
Conversation
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 SummaryThis PR fixes a real zsh word-splitting bug in the orphaned-worktree loop of
Confidence Score: 5/5Safe 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
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"]
%%{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"]
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.
Summary
zsh, notbash. Unlike bash, zsh does not word-split an unquoted multi-line variable by default —for dir in $ORPHANED_DIRScollapses every orphaned worktree directory into a single loop iteration whose$diris the whole newline-joined blob the moment more than one orphaned directory exists, breaking thegit -C "$dir"lookup with a "no such file or directory" error./housekeepcoordinator for real and hitting the identical pattern in its own Phase 3 loop (already fixed there).printf '%s\n' "$ORPHANED_DIRS" | while IFS= read -r dir; do ... done, which is portable across bash and zsh.lint-skill.sh/smoke-test-skill.shagainst this file surfaced several pre-existing, unrelated findings (missing## Examplessection, missing per-phaseExit conditionlines, a few un-commented2>/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)forunder zsh collapses to one iteration) and confirmed the new pattern fixes it