What Actually Happens When a New Shell Starts
Oh My Zsh is easiest to understand by following one startup from beginning to end. This chapter focuses on the path from the installer and the user’s .zshrc to completion, plugins, custom files, and the prompt. The practical payoff is a causal model: when a setting is ignored, a plugin is missing, or startup degrades, you can ask which stage owns the problem instead of treating the framework as one opaque block.
From a remote checkout to a user-controlled startup file
Installation begins outside the interactive shell, but its purpose is to change what future interactive shells do. The installer checks that Zsh is available, obtains the Oh My Zsh repository with Git, and chooses where to place it. The default location is under the user’s home directory, but the path can be changed. The installer also accepts a repository, remote URL, and branch, which means the checkout is not conceptually limited to one GitHub copy. A fork or alternate branch can enter the same startup boundary, although the project does not promise that every such source behaves like the official one.
The installer does not simply drop files into place and hope the next shell finds them. It creates a new configuration from the bundled template. If a .zshrc already exists, it moves that file to a known backup name before writing the new configuration. If an earlier backup already exists, it preserves that older backup with a timestamp rather than silently replacing it. The previous login shell is also recorded when the installer is preparing to change the default shell. These steps give the user an exit path, but they do not make the operation invisible: the installer is changing both a startup file and, if approved, the program used for login sessions.
Several controls narrow that change. A user can keep the existing .zshrc, skip the shell change, or use unattended mode, which avoids interactive shell switching and avoids launching a new Zsh at the end. A non-interactive invocation is treated conservatively by the installer as unattended. If the destination already exists, the installer stops rather than assuming it is safe to overwrite an existing checkout. If Zsh is unavailable, installation stops before creating a configuration that cannot run. If the system has no usable chsh path, the installer explains that the shell must be changed manually rather than pretending the login-shell transition succeeded.
The generated file is the handoff point. It sets the installation path, chooses a theme, exposes options for completion and prompt behavior, defines update preferences, lists selected plugins, and then sources the framework’s main initialization file. Personal configuration follows that source line. In other words, the file has a before-and-after boundary: settings that control initialization must be visible before the handoff, while personal aliases and environment choices can be placed afterward and can override defaults that were already loaded.
This explains why a user who keeps an existing .zshrc still needs to understand the connection. Keeping the file avoids replacing it; it does not automatically point the shell at Oh My Zsh. The configuration must still provide the relevant path and source the framework. Conversely, accepting the template does not transfer ownership of the whole shell to the project. The template is designed to be edited: the plugin list is a selection, the theme is a choice, and the post-source area remains available for personal behavior.
The installation stage therefore establishes three kinds of state. The repository is the maintained code that will be loaded. The startup file is the user’s declaration of which parts of that code participate. The backup files are evidence of what existed before the integration. Keeping those states distinct is what makes adoption understandable and, within limits, reversible.
The startup pipeline has an order
Once Zsh reads the source line, Oh My Zsh performs a sequence of responsibilities. The sequence is not an implementation detail that can be rearranged without consequence; it determines what is discoverable, what state is prepared, and which definition wins when multiple locations contain similarly named code.
1. Establish the runtime boundary
The entry point first protects itself from being run by the wrong shell. If the current process is not Zsh, it reports the problem and shows enough process information to explain how the file was reached. It also rejects an unsupported emulation mode. These checks are useful because shell syntax and shell features are not interchangeable: a file that expects Zsh arrays, functions, completion, and prompt expansion cannot be made reliable merely by giving it a shell-like name.
Next, the runtime resolves the installation path, the custom configuration path, and the cache path. If no custom location is supplied, the custom area is inside the installation. If the default cache directory is not writable, the runtime falls back to a cache location under the user’s home or the platform’s cache convention. It creates a completion subdirectory and adds it to Zsh’s function path, or fpath, so generated completion data can be found.
The update check occurs early, before the rest of initialization. It may decide that no check is due, that updates are disabled, that the installation is not writable or owned by the user, that the process is not interactive, or that Git is unavailable. Those conditions end the update path; they do not necessarily stop the rest of Oh My Zsh from loading. This is an important distinction between a conditional maintenance task and the main shell initialization path.
2. Make selected capabilities discoverable
The user’s plugin array determines which optional modules should participate. For each selected name, the runtime looks for a conventional plugin entry point or a completion function. It adds the matching plugin directory to fpath before running Zsh’s completion initialization. If no matching plugin is found, it reports that name and continues rather than silently claiming that the selection worked.
The order matters because completion is not a lookup that waits until the first Tab press to discover everything. compinit builds a working completion environment from the function path it sees. If a selected plugin’s completion definitions are not present when that environment is prepared, the plugin cannot be treated as a normal participant in completion merely because it is sourced later. Oh My Zsh therefore puts plugin discovery ahead of completion initialization.
This is the first clear example of a configuration setting acting as a trigger. Adding a name to the array does two things: it asks the runtime to source that plugin later, and it asks the completion system to consider the plugin’s directory now. A missing name, a misspelled name, or a plugin that does not follow the expected layout can fail at this boundary even if the rest of the framework is healthy.
The function path also separates two questions that are easy to conflate. First, can Zsh discover a function or completion definition? Second, which file will Oh My Zsh source for the named library or plugin? The fpath preparation answers the first question, while the loader’s custom-first checks answer the second. A custom plugin can therefore be made visible for completion and selected in place of the stock entry point, while an unrelated custom top-level file is loaded later through a different mechanism. Treating all custom behavior as one undifferentiated override hides these separate points of control.
The timing also explains why changing a plugin list can have effects beyond the next line of the startup file. The plugin directory must be visible before completion is initialized, and the resulting function path becomes part of the metadata used to judge whether the completion dump is current. Adding or removing a selected plugin can therefore cause the saved completion state to be rebuilt on a later startup. The rebuild is not evidence of failure; it is the framework reconciling persistent completion state with a changed set of inputs.
The same reasoning helps interpret a partial success. Oh My Zsh can report that one selected plugin is missing and continue to load other libraries and plugins. It can find a plugin but withhold completion from an insecure directory. It can fail to find a theme and leave the shell running with a different prompt. These outcomes are materially different from the entry point rejecting the wrong shell or an error that prevents the configuration from reaching later stages. A visible prompt proves only that enough of the pipeline completed; it does not prove that every requested capability did.
3. Prepare and validate completion state
Zsh’s completion setup can be expensive enough that the result is saved in a completion dump. Oh My Zsh gives that dump a host- and Zsh-version-specific default name and records metadata about the repository revision and the function path. When those metadata lines no longer match, the dump is removed and rebuilt. When the compiled form is older than the source dump, the runtime recompiles it, using a lock directory so competing startup processes do not perform the same work at once.
The runtime also checks completion-related permissions. By default, it uses compaudit and secure completion initialization. If completion-dependent directories have ownership or permission problems, Oh My Zsh reports them and withholds completion from those locations until they are fixed. A user can explicitly disable the check, but that is a change in the security boundary, not merely a way to silence a message. The framework is choosing not to load code from a directory it considers insecure unless the user overrides that decision.
The completion library adds another layer of state. It configures matching behavior, menu selection, caching for completion sources, and a cache path. The result is a more responsive interface after preparation, but it depends on persistent data that can become stale or invalid when the function path, repository revision, host, or permissions change. “Completion feels immediate” is therefore the visible result of work performed earlier and of checks that decide what is allowed to participate.
4. Load shared behavior, selected modules, and user overrides
After completion is prepared, the runtime loads the framework’s library files. These libraries establish shared functions and defaults for areas such as appearance, terminal titles, directory reporting, and Git-aware prompt support. It then loads the selected plugins. A plugin can add aliases, functions, completion definitions, hooks, or variables, all in the current Zsh process.
The loader uses custom-first resolution for library and plugin paths. If a corresponding file exists in the custom location, it is sourced instead of the stock file. This is how a user can override a bundled plugin without editing the maintained checkout. The choice also creates hidden coupling: code with a familiar plugin name may not be the code a reader expects, and a custom definition can affect later prompt or completion behavior because everything shares one shell environment.
After selected plugins, the runtime loads top-level custom .zsh files. These files are a broad user extension point rather than a named plugin interface. Finally, it resolves and loads the selected theme, checking custom theme locations before the bundled themes. The theme comes after the libraries and plugins because it can call prompt functions that those earlier layers define. Loading a theme earlier would make those functions unavailable or force the theme to recreate their behavior.
The template’s personal configuration area comes after the framework source line. That gives the user a final place to define personal aliases, environment variables, and other settings. It does not mean every later assignment will override every earlier behavior; shell functions, hooks, prompt substitutions, and completion styles have their own interactions. It does mean the configuration’s visible order communicates ownership: upstream defaults are loaded first, then the user’s own choices can be applied.
5. Read failure as a stage, not a verdict
The pipeline supplies a useful diagnostic vocabulary. If Oh My Zsh says it cannot load from the current process, the problem is the shell boundary. If a plugin is reported missing, the issue is selection or path resolution, not necessarily completion. If completion is withheld, inspect ownership and permissions before assuming the plugin failed. If a theme is present but symbols render incorrectly, check the terminal and font boundary. If a standard plugin behaves unexpectedly, check whether a custom same-name module is taking precedence.
This staged model does not guarantee that every problem has one obvious cause. Shell configuration is shared state, and external plugins or user files can change variables and functions outside the framework’s direct control. The point is narrower and more practical: Oh My Zsh is not a single switch. It is a sequence with ownership boundaries. Understanding the sequence makes the next question possible: what responsibilities do the loaded modules actually bring into the shell, and which of them should remain part of a personal workflow?