Skip to content

fix(install): wb.upgrade brick — symlink corruption + zsh $path clobber#17

Merged
amit-t merged 1 commit into
mainfrom
fix/install-symlink-and-update-zsh-path-clobber
May 14, 2026
Merged

fix(install): wb.upgrade brick — symlink corruption + zsh $path clobber#17
amit-t merged 1 commit into
mainfrom
fix/install-symlink-and-update-zsh-path-clobber

Conversation

@amit-t
Copy link
Copy Markdown
Owner

@amit-t amit-t commented May 14, 2026

Summary

Two install/upgrade bugs surfaced during the v1.1.0 real-LLM smoke against wb-gitlore. Together they made every wb.upgrade a no-op that wrongly reported a clean workbench as dirty and forwarded into a deleted worktree path. Both reproducible on macOS+zsh.

Bug 1 — install.zsh corrupts update.zsh via legacy symlink

install.zsh:142 does:

cat > "$BIN_DIR/update.wb" <<SH
...
SH

Earlier install.zsh versions installed update.wb as a symlink (~/.local/bin/update.wb → update-workbench/update.zsh). The new cat > follows that symlink and overwrites the real update.zsh with the 3-line deprecation shim. Every re-install re-corrupts the clone:

#!/usr/bin/env zsh
print -u2 -r -- "[deprecated] use 'wb.upgrade'. Forwarding..."
exec "/.../.claude/worktrees/feat+repo-context-scan/update-workbench/update.zsh" "$@"

(The hard-coded worktree path is whatever $SCRIPT_DIR was the first time cat > ran, frequently a long-deleted feature worktree.)

Fix: rm -f "$DEPRECATED_SHIM" before the heredoc so any legacy symlink is severed first.

Bug 2 — update.zsh clobbers $PATH via zsh $path special parameter

update-workbench/update.zsh:146 does while IFS= read -r path; do ... done. In zsh, the lowercase $path is a tied array that mirrors $PATH. Reading the first template path into path rewrites PATH to that value (e.g. CLAUDE.md). Every subsequent git call in the loop hits "command not found" (exit 127), so git diff --quiet returns non-zero and every template-owned path is falsely flagged as dirty:

Uncommitted changes on template-owned paths:
  CLAUDE.md
  AGENTS.md
  ...
  tests/**
Commit or stash them, or open a PR upstream for the improvements, then retry.

The user is told to commit changes that don't exist.

Fix: rename the loop variable from path to tpl_path in all three while-loops (dirty check, dry-run diff stat, apply). Drop a one-line note in the dirty-check block so the next reader doesn't trip over the same gotcha.

Regression tests

  • tests/test-install-update-wb-symlink-safe.zsh — sets up a fake HOME with the legacy update.wb symlink, runs install.zsh, and asserts the real update.zsh is byte-identical afterwards (shasum compare) and that the new shim is a regular file, not a symlink.
  • tests/test-update-zsh-path-clobber.zsh — drives update.zsh --dry-run against a fresh stamped workbench built from tests/fixtures/bare-upstream/setup.sh, with one clean template-owned path (version.json). Asserts the dirty-check does not falsely flag the path and --dry-run reaches the diff stage.

Both tests were verified red against the unfixed code and green after the fixes. tests/run-all.zsh picks them up automatically.

Test plan

  • CI green (shell-lint + tests)
  • After merge + release: wb.upgrade from a stamped wb correctly detects a clean tree, pulls template-owned paths, and stamps .workbench-state/template-version.json
  • install.zsh re-run on a host with the legacy symlink does not corrupt update.zsh (test catches this)

Risk

Low. Both fixes are surgical, with regression tests. The tpl_path rename does not change any external interface. The rm -f before cat > is a no-op if the path didn't already exist.

🤖 Generated with Claude Code

Two related bugs surfaced during the v1.1.0 real-LLM smoke against
wb-gitlore. Together they made every wb.upgrade invocation a no-op
that flagged the workbench as "dirty" and forwarded into a broken
worktree path.

1. install.zsh:142 — `cat > "$BIN_DIR/update.wb"` followed the legacy
   symlink that older install.zsh versions created (`~/.local/bin/update.wb
   -> $SCRIPT_DIR/update-workbench/update.zsh`). The heredoc was written
   into the *real* update.zsh, replacing the 244-line script with a
   3-line self-referential forwarder pointing at a deleted worktree.
   Fix: `rm -f "$DEPRECATED_SHIM"` severs any legacy symlink before
   writing the shim.

2. update-workbench/update.zsh:146,165,175 — `while IFS= read -r path`
   clobbered zsh's lowercase `$path` special parameter, which is a tied
   array mapped to `$PATH`. First iteration replaced PATH with the
   loop value (e.g. "CLAUDE.md"). Every subsequent `git diff --quiet`
   in the dirty-check loop exited 127 ("command not found"), so every
   template-owned path was falsely flagged as dirty and wb.upgrade
   bailed with exit 2.
   Fix: rename the loop variable from `path` to `tpl_path` in all three
   loops (dirty check, dry-run diff, apply).

Both bugs are reproducible-on-macOS-zsh and were caught by manual
smoke against a real workbench instance. Added two regression tests:

- tests/test-install-update-wb-symlink-safe.zsh — sets up a fake HOME
  with the legacy symlink and asserts install.zsh writes a regular
  file (not through the symlink).
- tests/test-update-zsh-path-clobber.zsh — drives update.zsh --dry-run
  through a clean stamped workbench and asserts the dirty-check does
  not falsely flag template-owned paths.

Both tests verified red-state before the fix and green-state after.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@amit-t amit-t merged commit 8b6ee77 into main May 14, 2026
12 checks passed
@amit-t amit-t deleted the fix/install-symlink-and-update-zsh-path-clobber branch May 14, 2026 03:54
amit-t added a commit that referenced this pull request May 19, 2026
fix(version-check): recover devkit clone when DEVKIT_CLONE env unset (#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