Skip to content

Installer overwrites ~/.oh-my-zsh with a static copy, breaking omz update #187

Description

@eezzekl

Problem

The repository ships a complete, frozen copy of Oh My Zsh at GentlemanZsh/.oh-my-zsh (1019 files, ~11 MB), and the installer copies it file by file over ~/.oh-my-zsh, whether or not a previous installation exists.

Oh My Zsh is a Git repository that updates itself. Overwriting its tracked files makes every difference between the bundled copy and upstream's actual state register as a local modification to a tracked file. The next Oh My Zsh auto-update then conflicts.

Where it happens

installer/internal/tui/installer.go:804:

if err := system.CopyDir(filepath.Join(repoDir, "GentlemanZsh", ".oh-my-zsh"), filepath.Join(homeDir, ".oh-my-zsh")); err != nil {

system.CopyDir (installer/internal/system/exec.go:306) walks the source tree and writes every file with CopyFile. It overwrites the destination without comparing versions, without excluding anything, and without checking whether Oh My Zsh is already installed on the system. Files that exist in the copy but no longer exist upstream are left behind, resurrected.

Causal chain

  1. The user has Oh My Zsh installed the official way, so ~/.oh-my-zsh is a Git clone.
  2. The installer overwrites tracked files: lib/, plugins/, themes/, tools/, oh-my-zsh.sh.
  3. Those files become dirty relative to the Git index.
  4. tools/upgrade.sh runs with rebase.autoStash true (line 226) and executes git pull --quiet --rebase (line 245).
  5. The autostash stores the modifications the installer left behind, rebases onto upstream, and the pop collides with upstream's own changes to the same files.
  6. The Oh My Zsh update is left broken and requires manual conflict resolution.

Evidence

Real installation performed about a month ago, on top of a pre-existing Oh My Zsh installed the official way.

HEAD commit left in ~/.oh-my-zsh after the first auto-update following the installation:

3036697 resolve: bundler stash pop conflict (upstream 59a9740)
        + legacy Gentleman.Dots bundle_install() commented

Comparison between the bundled copy and the live clone: 38 divergences. Tracked files that differ include:

plugins/bundler/bundler.plugin.zsh
plugins/flutter/flutter.plugin.zsh
plugins/flutter/README.md
plugins/rails/rails.plugin.zsh
plugins/rails/_rails
plugins/rails/README.md
plugins/pm2/_pm2

Directories upstream deleted that the bundled copy recreated, and that now sit as untracked files inside the clone:

?? plugins/fig/
?? plugins/rbfu/

Secondary defect

The bundled copy contains no .git. On a clean installation, on a system without a prior Oh My Zsh, the result is a ~/.oh-my-zsh that is not a Git repository. That Oh My Zsh can never update, and it fails silently: omz update has no remote to pull from.

There are two scenarios and neither ends well:

Prior state Current result
Oh My Zsh already installed (Git clone) Rebase conflicts on every auto-update
No Oh My Zsh Oh My Zsh without Git, unable to update, no warning

Expected behavior

The installation should leave a working, updatable Oh My Zsh in both scenarios, and contribute only Gentleman.Dots' own configuration.

Actual behavior

The installation leaves an Oh My Zsh whose official update produces rebase conflicts, or an Oh My Zsh with no ability to update at all.

Proposal

Oh My Zsh is genuinely required: GentlemanZsh/.zshrc:117 runs source $ZSH/oh-my-zsh.sh. What is not required is keeping it vendored inside the repository. The project's configuration enables a single plugin (GentlemanZsh/.zshrc:113-115):

plugins=(
  command-not-found
)

The 1019 bundled files exist to serve the Oh My Zsh core plus one plugin. The proposal has two parts.

1. Detect before writing

The installer should check whether ~/.oh-my-zsh already exists. If it does, leave it alone: that installation belongs to Oh My Zsh and its update cycle, not to the installer.

2. Delegate installation to the official installer

When ~/.oh-my-zsh does not exist, install Oh My Zsh through its official path instead of copying the frozen snapshot:

RUNZSH=no CHSH=no KEEP_ZSHRC=yes sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

All three variables are documented in Oh My Zsh's own tools/install.sh:28-35. KEEP_ZSHRC=yes is required so the official installer does not replace .zshrc with its template; Gentleman.Dots copies its own immediately afterwards and keeps control of the configuration.

This introduces no new dependency. The installer already clones repositories and runs remote installers in several places, among them:

  • installer/internal/tui/installer.go:122 — clones Gentleman.Dots itself
  • installer/internal/tui/installer.go:935git clone https://github.com/tmux-plugins/tpm
  • installer/internal/tui/installer.go:337 — clones Alacritty
  • installer/internal/tui/installer.go:157 — Homebrew installer via curl

The tpm case is the exact precedent for the proposed pattern: a third-party tool fetched from its origin instead of being vendored into the repository.

Resulting flow

Does ~/.oh-my-zsh exist?
├── Yes → leave it untouched
└── No  → official Oh My Zsh installer
Then, in both cases: copy .zshrc, .p10k.zsh and whatever belongs in custom/

3. Drop the bundled copy

With the above in place, GentlemanZsh/.oh-my-zsh no longer serves a purpose, except for whatever genuinely belongs under $ZSH_CUSTOM (~/.oh-my-zsh/custom/), which is the extension point Oh My Zsh provides for exactly this kind of third-party configuration.

Verification: the bundled copy contains no project-owned code

Before proposing its removal, I checked that there is no project-owned function or intentional modification inside the copy. The method: hash every bundled file and verify whether that exact blob exists in the official Oh My Zsh repository's history.

h=$(git hash-object "GentlemanZsh/.oh-my-zsh/$f")
git -C ~/.oh-my-zsh cat-file -e "$h"   # if it exists, the file is unmodified upstream

Result across all 1019 files: 1015 match an identical upstream blob. The remaining 4 are plugins/fig/ and plugins/rbfu/, also upstream files, whose blobs are missing only because their deletion predates the locally available history.

Two further confirmations:

  • rg -i gentleman GentlemanZsh/.oh-my-zsh returns no matches.
  • The directory has 3 commits in the entire repository history, all of them imports (fix: add oh-my-zsh files directly, fix: new try, fix: add oh-my-zsh directory). There was never a deliberate edit.

Therefore, dropping the copy loses no project functionality and there is nothing to migrate into $ZSH_CUSTOM.

A note on bundle_install()

While resolving these conflicts it is easy to mistake old Oh My Zsh code for Gentleman.Dots code. A concrete case: in the bundled copy, plugins/bundler/bundler.plugin.zsh contains a bundle_install() function alongside alias bi="bundle_install". That function is deprecated Oh My Zsh code, not a Gentleman.Dots function: upstream removed it and now defines alias bi="bundle install". The blob for that file exists verbatim in upstream history.

Worth flagging, because anyone resolving the conflict by hand may keep or label as project-owned code that should simply take upstream's version.

Alternatives considered

Current Detect only, keep the copy Detect + official installer
Existing installation ❌ conflicts
Clean installation, updatable ❌ frozen Oh My Zsh, no Git
Third-party copy to maintain Yes Yes No
Repository size unchanged −11 MB
New dependencies none none

Keeping the copy would only be justified to support offline installation, and that is not the case here: the installer fetches itself from GitHub and depends on the network for package managers, fonts and terminals.

Note on remediating existing installations

Anyone who already installed and has the Git clone can restore upstream's clean state, bearing in mind this discards any local modification inside ~/.oh-my-zsh:

git -C ~/.oh-my-zsh fetch origin
git -C ~/.oh-my-zsh reset --hard origin/master
git -C ~/.oh-my-zsh clean -fd

Environment

  • Gentleman.Dots v2.12.2
  • Linux (WSL2)
  • Zsh with Oh My Zsh previously installed the official way
  • Affected installer branch: case "zsh" in installer/internal/tui/installer.go:770-807

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions