Skip to content

fix: prevent recursive sourcing in zsh migration overlays#26

Merged
alohays merged 3 commits into
mainfrom
fix/strip-framework-lines-from-zsh-migration
Mar 22, 2026
Merged

fix: prevent recursive sourcing in zsh migration overlays#26
alohays merged 3 commits into
mainfrom
fix/strip-framework-lines-from-zsh-migration

Conversation

@alohays
Copy link
Copy Markdown
Owner

@alohays alohays commented Mar 22, 2026

Summary

  • Strip dotfiles framework infrastructure lines (dotfiles_source_*, lib.sh bootstrap, DOTFILES_HOME defaults, managed wrapper boilerplate) during zsh content migration to prevent infinite recursion
  • Skip creating overlay files when sanitised content is effectively empty (only whitespace and comments remain)
  • Add unit and integration tests covering canonical file content, mixed user+framework content, and managed wrapper content

Problem

When the user's old ~/.zshenv was a symlink to the repo's canonical zsh/zshenv, the migration copied framework sourcing infrastructure into local.zshenv.sh. This included dotfiles_source_optional_relaxed "...local.zshenv.sh", causing the file to source itself infinitely (same for .zshrclocal.zsh.zsh and .zprofilelocal.zprofile.sh).

Symptoms on every shell startup:

dotfiles_os_name:4: write error: bad file descriptor
cat: stdout: Bad file descriptor
/Users/.../.config/dotfiles/env.sh:.:3: too many open files: .../lib.sh

Root cause

sanitize_migrated_zsh_content() stripped obsolete plugin manager patterns (antidote, prezto, zinit) but not the framework's own sourcing lines. When the backed-up file was the framework's canonical file, every line passed through the sanitiser unchanged — including the self-referencing overlay source.

Changes

scripts/migrate_zsh.py:

  • Add 14 line-strip patterns and 1 block-strip pattern for framework infrastructure
  • Add _has_meaningful_content() helper to detect empty-after-sanitisation results
  • Skip overlay creation when sanitised content has no meaningful lines

tests/test_dotfiles_apply.py:

  • 5 new unit tests in SanitizeMigratedZshContentTests (canonical zshenv/zshrc/zprofile stripping, mixed content preservation, managed wrapper stripping)
  • 1 new integration test in DotfilesApplyTests (apply skips migration for pure-framework content)

Test plan

  • python3 -m unittest discover -s tests -p 'test_*.py' — all 88 tests pass
  • On a clean HOME, set .zshenv as symlink to zsh/zshenv, run dotfiles apply → verify local.zshenv.sh is NOT created
  • On a clean HOME with .zshenv containing framework + user PATH lines → verify overlay has only user lines
  • Open a new terminal after fix and confirm no recursion errors

🤖 Generated with Claude Code

alohays and others added 2 commits March 22, 2026 18:00
When the user's old ~/.zshenv was a symlink to the repo's canonical
zsh/zshenv, the migration copied framework sourcing infrastructure
into the local overlay file. This included a line that sources the
overlay itself, causing infinite recursion and "too many open files"
errors on every shell startup.

Add strip patterns for framework-owned lines (dotfiles_source_*,
lib.sh bootstrap, DOTFILES_HOME defaults, managed wrapper boilerplate)
and skip creating overlay files when sanitised content is effectively
empty (only whitespace and comments remain).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add unit tests verifying that canonical zshenv, zshrc, and zprofile
content is fully stripped during migration, that user content mixed
with framework lines is preserved, and that managed wrapper content
is stripped. Add integration test confirming apply skips overlay
creation when legacy content is pure framework.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@alohays
Copy link
Copy Markdown
Owner Author

alohays commented Mar 22, 2026

Code review

Found 2 issues:

  1. Stale user-facing header in migrated overlay files. Line 275 still reads "# Known-obsolete patterns (antidote, prezto, GLOBAL_RCS) were stripped." but this PR adds an entirely new stripping category (framework-owned sourcing infrastructure: dotfiles_source_*, lib.sh bootstrap, DOTFILES_HOME defaults, managed wrapper boilerplate). Users reading their migrated local.zshenv.sh will not know that framework lines were also removed.

f"# Auto-migrated from backed-up {target_rel} on {_utc_now()}.\n"
"# Known-obsolete patterns (antidote, prezto, GLOBAL_RCS) were stripped.\n"
"# Review and trim this file -- the framework handles PATH, plugins, and completion.\n\n"

  1. .profile-sourcing strip pattern may false-positive on user content. The pattern at line 132 strips [ -r "$HOME/.profile" ] && . "$HOME/.profile", which exists in the canonical zsh/zprofile. However, this is also idiomatic POSIX shell code that users commonly write themselves in .zprofile. A user who independently had this line pre-framework would have it silently removed during migration, with no indication in the header comment.

re.compile(r"^\s*\[\[\s+-o\s+interactive\s*\]\]\s*\|\|\s*return"),
re.compile(r"""^\s*\[\s+-r\s+["']\$HOME/\.profile["']\s*\]\s*&&\s*\."""),
re.compile(r"^#\s*Canonical repo-owned zsh"),

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

…stripping

The auto-migration header still read "Known-obsolete patterns (antidote,
prezto, GLOBAL_RCS) were stripped" after cb0c03e added framework-owned
line stripping. Update the user-facing header and module docstring to
cover both categories so migrated overlay files accurately describe
what was removed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@alohays
Copy link
Copy Markdown
Owner Author

alohays commented Mar 22, 2026

Review comment resolution & manual test results

What was addressed

Issue 1 (stale header comment) — Fixed (74f7d0a)

  • Migration header updated to: "Obsolete plugin-manager and dotfiles-framework lines were stripped."
  • Module docstring updated to mention framework-owned sourcing infrastructure

Issue 2 (.profile sourcing false positive) — Not addressed

  • The canonical zsh/zprofile already runs [ -r "$HOME/.profile" ] && . "$HOME/.profile", so keeping this line in the overlay would cause .profile to be double-sourced
  • Therefore, stripping it is the correct behavior even if the user wrote the line independently
  • Removing the pattern would cause a regression where canonical zprofile content leaks into overlays
  • Backups are preserved so users can verify originals, and the Issue 1 header fix improves transparency

Manual test simulation results (3/3 PASS)

Simulated in isolated temp HOME directories using python3 scripts/dotfiles.py apply --repo-root . --home <tmpdir> --profile base:

Test A: clean HOME + canonical .zshenv → overlay not created

  • Placed zsh/zshenv canonical content in .zshenv and ran apply
  • Confirmed local.zshenv.sh was NOT created
  • Note: "legacy .zshenv contained only framework-owned lines; skipped creating .config/dotfiles/local.zshenv.sh"

Test B: clean HOME + mixed framework+user .zshenv → only user lines remain

  • Mixed canonical lines with export PATH="$HOME/bin:$PATH" and export EDITOR=nvim
  • Overlay contained only user lines; all framework lines were stripped
  • New header "Obsolete plugin-manager and dotfiles-framework lines were stripped." displayed correctly

Test C: no recursive sourcing errors

  • Ran zsh -c 'source "$HOME/.zshenv"' in both Test A and Test B environments
  • No too many open files / bad file descriptor errors, exit code 0

Temp directories cleaned up after testing.

🤖 Generated with Claude Code

@alohays alohays merged commit f391124 into main Mar 22, 2026
2 checks passed
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