From f78d1dcd221c0d42aa01d304938b07e81e1efc05 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Thu, 26 Mar 2026 23:52:15 -0600 Subject: [PATCH] fix: replace zsh-incompatible raw globs with find-based alternatives and setopt guards Zsh's NOMATCH option (on by default) causes raw globs like `*.yaml` and `*deploy*` to throw errors when no files match, instead of silently expanding to nothing as bash does. The preamble resolver already handled this correctly with find, but 38 glob instances across 13 templates and 2 resolvers still used raw shell globs. Two fix approaches based on complexity: - find-based replacement for cat/for/ls-with-pipes patterns (.github/workflows/) - setopt +o nomatch guard for simple ls -t patterns (~/.gstack/, ~/.claude/) Co-Authored-By: Claude Opus 4.6 (1M context) --- codex/SKILL.md.tmpl | 1 + cso/SKILL.md.tmpl | 7 ++++--- design-consultation/SKILL.md.tmpl | 1 + land-and-deploy/SKILL.md.tmpl | 8 +++++--- office-hours/SKILL.md.tmpl | 3 +++ plan-ceo-review/SKILL.md.tmpl | 3 +++ plan-eng-review/SKILL.md.tmpl | 1 + qa-only/SKILL.md.tmpl | 1 + qa/SKILL.md.tmpl | 1 + retro/SKILL.md.tmpl | 5 +++++ scripts/resolvers/testing.ts | 2 ++ scripts/resolvers/utility.ts | 2 +- setup-deploy/SKILL.md.tmpl | 4 ++-- 13 files changed, 30 insertions(+), 9 deletions(-) diff --git a/codex/SKILL.md.tmpl b/codex/SKILL.md.tmpl index 4a8fbbe8..3103387f 100644 --- a/codex/SKILL.md.tmpl +++ b/codex/SKILL.md.tmpl @@ -240,6 +240,7 @@ TMPERR=$(mktemp /tmp/codex-err-XXXXXX.txt) 3. **Plan review auto-detection:** If the user's prompt is about reviewing a plan, or if plan files exist and the user said `/codex` with no arguments: ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat ls -t ~/.claude/plans/*.md 2>/dev/null | xargs grep -l "$(basename $(pwd))" 2>/dev/null | head -1 ``` If no project-scoped match, fall back to `ls -t ~/.claude/plans/*.md 2>/dev/null | head -1` diff --git a/cso/SKILL.md.tmpl b/cso/SKILL.md.tmpl index b1904a8e..676c1bd9 100644 --- a/cso/SKILL.md.tmpl +++ b/cso/SKILL.md.tmpl @@ -73,7 +73,7 @@ ls go.mod 2>/dev/null && echo "STACK: Go" ls Cargo.toml 2>/dev/null && echo "STACK: Rust" ls pom.xml build.gradle 2>/dev/null && echo "STACK: JVM" ls composer.json 2>/dev/null && echo "STACK: PHP" -ls *.csproj *.sln 2>/dev/null && echo "STACK: .NET" +find . -maxdepth 1 \( -name '*.csproj' -o -name '*.sln' \) 2>/dev/null | grep -q . && echo "STACK: .NET" ``` **Framework detection:** @@ -110,7 +110,8 @@ Map what an attacker sees — both code surface and infrastructure surface. **Infrastructure surface:** ```bash -ls .github/workflows/*.yml .github/workflows/*.yaml .gitlab-ci.yml 2>/dev/null | wc -l +setopt +o nomatch 2>/dev/null || true # zsh compat +{ find .github/workflows -maxdepth 1 \( -name '*.yml' -o -name '*.yaml' \) 2>/dev/null; [ -f .gitlab-ci.yml ] && echo .gitlab-ci.yml; } | wc -l find . -maxdepth 4 -name "Dockerfile*" -o -name "docker-compose*.yml" 2>/dev/null find . -maxdepth 4 -name "*.tf" -o -name "*.tfvars" -o -name "kustomization.yaml" 2>/dev/null ls .env .env.* 2>/dev/null @@ -160,7 +161,7 @@ grep -q "^\.env$\|^\.env\.\*" .gitignore 2>/dev/null && echo ".env IS gitignored **CI configs with inline secrets (not using secret stores):** ```bash -for f in .github/workflows/*.yml .github/workflows/*.yaml .gitlab-ci.yml .circleci/config.yml; do +for f in $(find .github/workflows -maxdepth 1 \( -name '*.yml' -o -name '*.yaml' \) 2>/dev/null) .gitlab-ci.yml .circleci/config.yml; do [ -f "$f" ] && grep -n "password:\|token:\|secret:\|api_key:" "$f" | grep -v '\${{' | grep -v 'secrets\.' done 2>/dev/null ``` diff --git a/design-consultation/SKILL.md.tmpl b/design-consultation/SKILL.md.tmpl index f33eabb6..2d7a5a34 100644 --- a/design-consultation/SKILL.md.tmpl +++ b/design-consultation/SKILL.md.tmpl @@ -53,6 +53,7 @@ ls src/ app/ pages/ components/ 2>/dev/null | head -30 Look for office-hours output: ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat {{SLUG_EVAL}} ls ~/.gstack/projects/$SLUG/*office-hours* 2>/dev/null | head -5 ls .context/*office-hours* .context/attachments/*office-hours* 2>/dev/null | head -5 diff --git a/land-and-deploy/SKILL.md.tmpl b/land-and-deploy/SKILL.md.tmpl index c22e99e5..acec63c2 100644 --- a/land-and-deploy/SKILL.md.tmpl +++ b/land-and-deploy/SKILL.md.tmpl @@ -113,7 +113,7 @@ else SAVED_HASH=$(cat ~/.gstack/projects/$SLUG/land-deploy-confirmed 2>/dev/null) CURRENT_HASH=$(sed -n '/## Deploy Configuration/,/^## /p' CLAUDE.md 2>/dev/null | shasum -a 256 | cut -d' ' -f1) # Also hash workflow files that affect deploy behavior - WORKFLOW_HASH=$(cat .github/workflows/*deploy* .github/workflows/*cd* 2>/dev/null | shasum -a 256 | cut -d' ' -f1) + WORKFLOW_HASH=$(find .github/workflows -maxdepth 1 \( -name '*deploy*' -o -name '*cd*' \) 2>/dev/null | xargs cat 2>/dev/null | shasum -a 256 | cut -d' ' -f1) COMBINED_HASH="${CURRENT_HASH}-${WORKFLOW_HASH}" if [ "$SAVED_HASH" != "$COMBINED_HASH" ] && [ -n "$SAVED_HASH" ]; then echo "CONFIG_CHANGED" @@ -223,7 +223,7 @@ grep -i "staging" CLAUDE.md 2>/dev/null | head -3 2. **GitHub Actions staging workflow:** Check for workflow files with "staging" in the name or content: ```bash -for f in .github/workflows/*.yml .github/workflows/*.yaml; do +for f in $(find .github/workflows -maxdepth 1 \( -name '*.yml' -o -name '*.yaml' \) 2>/dev/null); do [ -f "$f" ] && grep -qiE "staging" "$f" 2>/dev/null && echo "STAGING_WORKFLOW:$f" done ``` @@ -273,7 +273,7 @@ Save the deploy config fingerprint so we can detect future changes: ```bash mkdir -p ~/.gstack/projects/$SLUG CURRENT_HASH=$(sed -n '/## Deploy Configuration/,/^## /p' CLAUDE.md 2>/dev/null | shasum -a 256 | cut -d' ' -f1) -WORKFLOW_HASH=$(cat .github/workflows/*deploy* .github/workflows/*cd* 2>/dev/null | shasum -a 256 | cut -d' ' -f1) +WORKFLOW_HASH=$(find .github/workflows -maxdepth 1 \( -name '*deploy*' -o -name '*cd*' \) 2>/dev/null | xargs cat 2>/dev/null | shasum -a 256 | cut -d' ' -f1) echo "${CURRENT_HASH}-${WORKFLOW_HASH}" > ~/.gstack/projects/$SLUG/land-deploy-confirmed ``` Continue to Step 2. @@ -415,6 +415,7 @@ If tests fail: **BLOCKER.** Cannot merge with failing tests. **E2E tests — check recent results:** ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat ls -t ~/.gstack-dev/evals/*-e2e-*-$(date +%Y-%m-%d)*.json 2>/dev/null | head -20 ``` @@ -430,6 +431,7 @@ If E2E results exist but have failures: **WARNING — N tests failed.** List the **LLM judge evals — check recent results:** ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat ls -t ~/.gstack-dev/evals/*-llm-judge-*-$(date +%Y-%m-%d)*.json 2>/dev/null | head -5 ``` diff --git a/office-hours/SKILL.md.tmpl b/office-hours/SKILL.md.tmpl index 93abb1bb..c6de598f 100644 --- a/office-hours/SKILL.md.tmpl +++ b/office-hours/SKILL.md.tmpl @@ -48,6 +48,7 @@ Understand the project and the area the user wants to change. 3. Use Grep/Glob to map the codebase areas most relevant to the user's request. 4. **List existing design docs for this project:** ```bash + setopt +o nomatch 2>/dev/null || true # zsh compat ls -t ~/.gstack/projects/$SLUG/*-design-*.md 2>/dev/null ``` If design docs exist, list them: "Prior designs for this project: [titles + dates]" @@ -278,6 +279,7 @@ After the user states the problem (first question in Phase 2A or 2B), search exi Extract 3-5 significant keywords from the user's problem statement and grep across design docs: ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat grep -li "\|\|" ~/.gstack/projects/$SLUG/*-design-*.md 2>/dev/null ``` @@ -422,6 +424,7 @@ DATETIME=$(date +%Y%m%d-%H%M%S) **Design lineage:** Before writing, check for existing design docs on this branch: ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat PRIOR=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1) ``` If `$PRIOR` exists, the new doc gets a `Supersedes:` field referencing it. This creates a revision chain — you can trace how a design evolved across office hours sessions. diff --git a/plan-ceo-review/SKILL.md.tmpl b/plan-ceo-review/SKILL.md.tmpl index 71fbefde..404d1791 100644 --- a/plan-ceo-review/SKILL.md.tmpl +++ b/plan-ceo-review/SKILL.md.tmpl @@ -105,6 +105,7 @@ Then read CLAUDE.md, TODOS.md, and any existing architecture docs. **Design doc check:** ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat SLUG=$(~/.claude/skills/gstack/browse/bin/remote-slug 2>/dev/null || basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)") BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' || echo 'no-branch') DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1) @@ -115,6 +116,7 @@ If a design doc exists (from `/office-hours`), read it. Use it as the source of **Handoff note check** (reuses $SLUG and $BRANCH from the design doc check above): ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat HANDOFF=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-ceo-handoff-*.md 2>/dev/null | head -1) [ -n "$HANDOFF" ] && echo "HANDOFF_FOUND: $HANDOFF" || echo "NO_HANDOFF" ``` @@ -703,6 +705,7 @@ After producing the Completion Summary, clean up any handoff notes for this bran the review is complete and the context is no longer needed. ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat {{SLUG_EVAL}} rm -f ~/.gstack/projects/$SLUG/*-$BRANCH-ceo-handoff-*.md 2>/dev/null || true ``` diff --git a/plan-eng-review/SKILL.md.tmpl b/plan-eng-review/SKILL.md.tmpl index b4c47e4c..b1f05a03 100644 --- a/plan-eng-review/SKILL.md.tmpl +++ b/plan-eng-review/SKILL.md.tmpl @@ -68,6 +68,7 @@ When evaluating architecture, think "boring by default." When reviewing tests, t ### Design Doc Check ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat SLUG=$(~/.claude/skills/gstack/browse/bin/remote-slug 2>/dev/null || basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)") BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-' || echo 'no-branch') DESIGN=$(ls -t ~/.gstack/projects/$SLUG/*-$BRANCH-design-*.md 2>/dev/null | head -1) diff --git a/qa-only/SKILL.md.tmpl b/qa-only/SKILL.md.tmpl index 15d5fe4d..0bb59c0c 100644 --- a/qa-only/SKILL.md.tmpl +++ b/qa-only/SKILL.md.tmpl @@ -55,6 +55,7 @@ Before falling back to git diff heuristics, check for richer test plan sources: 1. **Project-scoped test plans:** Check `~/.gstack/projects/` for recent `*-test-plan-*.md` files for this repo ```bash + setopt +o nomatch 2>/dev/null || true # zsh compat {{SLUG_EVAL}} ls -t ~/.gstack/projects/$SLUG/*-test-plan-*.md 2>/dev/null | head -1 ``` diff --git a/qa/SKILL.md.tmpl b/qa/SKILL.md.tmpl index d228b21a..0283ffc7 100644 --- a/qa/SKILL.md.tmpl +++ b/qa/SKILL.md.tmpl @@ -96,6 +96,7 @@ Before falling back to git diff heuristics, check for richer test plan sources: 1. **Project-scoped test plans:** Check `~/.gstack/projects/` for recent `*-test-plan-*.md` files for this repo ```bash + setopt +o nomatch 2>/dev/null || true # zsh compat {{SLUG_EVAL}} ls -t ~/.gstack/projects/$SLUG/*-test-plan-*.md 2>/dev/null | head -1 ``` diff --git a/retro/SKILL.md.tmpl b/retro/SKILL.md.tmpl index cc4f53fa..5463d07a 100644 --- a/retro/SKILL.md.tmpl +++ b/retro/SKILL.md.tmpl @@ -307,6 +307,7 @@ Count backward from today — how many consecutive days have at least one commit Before saving the new snapshot, check for prior retro history: ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat ls -t .context/retros/*.json 2>/dev/null ``` @@ -333,6 +334,7 @@ mkdir -p .context/retros Determine the next sequence number for today (substitute the actual date for `$(date +%Y-%m-%d)`): ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat # Count existing retros for today to get next sequence number today=$(date +%Y-%m-%d) existing=$(ls .context/retros/${today}-*.json 2>/dev/null | wc -l | tr -d ' ') @@ -456,6 +458,7 @@ Narrative covering: Check review JSONL logs for plan completion data from /ship runs this period: ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" cat ~/.gstack/projects/$SLUG/*-reviews.jsonl 2>/dev/null | grep '"skill":"ship"' | grep '"plan_items_total"' || echo "NO_PLAN_DATA" ``` @@ -757,6 +760,7 @@ Considering the full cross-project picture. ### Global Step 8: Load history & compare ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat ls -t ~/.gstack/retros/global-*.json 2>/dev/null | head -5 ``` @@ -774,6 +778,7 @@ mkdir -p ~/.gstack/retros Determine the next sequence number for today: ```bash +setopt +o nomatch 2>/dev/null || true # zsh compat today=$(date +%Y-%m-%d) existing=$(ls ~/.gstack/retros/global-${today}-*.json 2>/dev/null | wc -l | tr -d ' ') next=$((existing + 1)) diff --git a/scripts/resolvers/testing.ts b/scripts/resolvers/testing.ts index fde799dc..da1381c2 100644 --- a/scripts/resolvers/testing.ts +++ b/scripts/resolvers/testing.ts @@ -6,6 +6,7 @@ export function generateTestBootstrap(_ctx: TemplateContext): string { **Detect existing test framework and project runtime:** \`\`\`bash +setopt +o nomatch 2>/dev/null || true # zsh compat # Detect project runtime [ -f Gemfile ] && echo "RUNTIME:ruby" [ -f package.json ] && echo "RUNTIME:node" @@ -200,6 +201,7 @@ Before analyzing coverage, detect the project's test framework: 2. **If CLAUDE.md has no testing section, auto-detect:** \`\`\`bash +setopt +o nomatch 2>/dev/null || true # zsh compat # Detect project runtime [ -f Gemfile ] && echo "RUNTIME:ruby" [ -f package.json ] && echo "RUNTIME:node" diff --git a/scripts/resolvers/utility.ts b/scripts/resolvers/utility.ts index c3d073f5..fed9e196 100644 --- a/scripts/resolvers/utility.ts +++ b/scripts/resolvers/utility.ts @@ -72,7 +72,7 @@ fi ([ -f railway.json ] || [ -f railway.toml ]) && echo "PLATFORM:railway" # Detect deploy workflows -for f in .github/workflows/*.yml .github/workflows/*.yaml; do +for f in $(find .github/workflows -maxdepth 1 \\( -name '*.yml' -o -name '*.yaml' \\) 2>/dev/null); do [ -f "$f" ] && grep -qiE "deploy|release|production|cd" "$f" 2>/dev/null && echo "DEPLOY_WORKFLOW:$f" [ -f "$f" ] && grep -qiE "staging" "$f" 2>/dev/null && echo "STAGING_WORKFLOW:$f" done diff --git a/setup-deploy/SKILL.md.tmpl b/setup-deploy/SKILL.md.tmpl index b4bd99ef..8326da97 100644 --- a/setup-deploy/SKILL.md.tmpl +++ b/setup-deploy/SKILL.md.tmpl @@ -64,13 +64,13 @@ Run the platform detection from the deploy bootstrap: [ -f railway.json ] || [ -f railway.toml ] && echo "PLATFORM:railway" # GitHub Actions deploy workflows -for f in .github/workflows/*.yml .github/workflows/*.yaml; do +for f in $(find .github/workflows -maxdepth 1 \( -name '*.yml' -o -name '*.yaml' \) 2>/dev/null); do [ -f "$f" ] && grep -qiE "deploy|release|production|staging|cd" "$f" 2>/dev/null && echo "DEPLOY_WORKFLOW:$f" done # Project type [ -f package.json ] && grep -q '"bin"' package.json 2>/dev/null && echo "PROJECT_TYPE:cli" -ls *.gemspec 2>/dev/null && echo "PROJECT_TYPE:library" +find . -maxdepth 1 -name '*.gemspec' 2>/dev/null | grep -q . && echo "PROJECT_TYPE:library" ``` ### Step 3: Platform-specific setup