- Profile
- What's in the Box?
- Installation
- New Mac
- Update
- Why not Oh-My-Zsh, Zim or Something else?
- Can I Use This Repository to Configure My Own Zsh?
- Does it Work with Bash and Other Shells Too?
This repository is used to manage and maintain my personal Zsh configuration preferences easily and across multiple environment. It is not designed to be generic and open, but it is mostly very simple and can be used as a reference for those who prefer to configure their own shell environment.
- common directory structures
- common environment variables
- aliases and shell functions
- utility scripts (added to
PATH) - Homebrew packages per machine profile (see: brew/Brewfile)
- language runtime versions for mise (see: config/mise/profile.toml)
- global instructions for coding agents (see: agents/AGENTS.md)
- zsh plugins (see: zsh-plugins)
- essential key bindings
- zsh completion configuration
git clone git@github.com:sha1n/profile.git
profile/install.shThe installation script runs these steps, each under a section title:
- Updates the git submodules.
- Links each file in the
dotfilesdirectory into your home directory. This takes care of.vimrc,.gitconfigand others.- If a file with the same name already exists, skips it
- Creates
~/.local/binand~/code/w. - On macOS, adds
brew shellenvto~/.zprofile, when Homebrew is installed and the line is not there yet. - Adds
source '<repo>/load.zsh'to~/.zshrc.- If the file doesn't exist, it creates it
- If
~/.zshrcalready sources aload.zsh, skips it
- Links
agents/AGENTS.mdto~/.agents/AGENTS.md,~/.claude/CLAUDE.mdand~/.codex/AGENTS.md.- If a target already exists, asks
[n/Y]before it replaces it. Default is Yes
- If a target already exists, asks
- Links the
config/nvimdirectory to~/.config/nvim.- If
~/.config/nvimis a directory of the old layout, whoseinit.lualinks into this repository, moves it to~/.config/nvim.bak.<epoch seconds>first - If anything else is at
~/.config/nvim, prints a warning and skips it
- If
- Links
config/mise/profile.tomlto~/.config/mise/conf.d/profile.toml. - Compiles the zsh files to
.zwcbytecode.
The script runs every step, even after one fails. An existing target is a skip, not a failure. At the end it exits with 1 and names the failed steps, or with 0.
The script installs no tools. Use profile install for that (see Update) or the bootstrap below.
One command sets up a new Mac. It installs Homebrew, clones this repository to ~/code/profile, runs install.sh, and runs profile install with the profiles that you name (default: essentials). It is macOS only and exits on other platforms. It can ask for your sudo password for the Homebrew installer. You can run it again: it skips an installed Homebrew and an existing clone.
curl -fsSL https://raw.githubusercontent.com/sha1n/profile/master/bootstrap.sh | zsh -s -- dev| Profile | Contents |
|---|---|
essentials |
CLI core, editors, git tools, terminal, personal apps, visual-studio-code, and uv for Python CLI tools |
dev |
Development tools and apps, and mise with the Go, Python, Node.js, Java and Maven runtimes. Includes essentials |
workstation |
Docker Desktop, for machines that do significant development work. Includes dev |
The profiles form one chain: essentials < dev < workstation. Each profile includes every profile before it. Slack and Zoom are not in any profile: install them by hand on the machines that need them.
mise owns the language runtimes. Their versions are in config/mise/profile.toml, which install.sh links to ~/.config/mise/conf.d/profile.toml. Homebrew installs no runtime. Python CLI tools install as uv tools (uv "<tool>" in the Brewfile, or uv tool install <tool>), not with pipx or a Homebrew formula. The shell exports PIP_REQUIRE_VIRTUALENV=true, so pip install works only inside a virtualenv.
The shell exports HOMEBREW_BUNDLE_FILE with the path of brew/Brewfile, so brew bundle, brew bundle check and brew bundle list read it from any directory. A plain brew bundle applies every profile. To install one profile, run profile install <profile>, or set HOMEBREW_PROFILE_INSTALL_PROFILES for one command, for example HOMEBREW_PROFILE_INSTALL_PROFILES=dev brew bundle check. A --file argument wins over the variable.
After the bootstrap, do these steps by hand: gh auth login, create an SSH key and add it with gh ssh-key add, and turn on VS Code Settings Sync. The bootstrap prints them at the end.
To let a coding agent set up a machine, tell it: "Set up this machine with the profile at https://github.com/sha1n/profile. Follow the 'Set up with an agent' section of its README."
Agent, do these steps in order. Each step ends only when its check passes.
-
Ask for the profiles. Show the user the profile table above, and ask which profile applies. Check: the user confirmed
essentials,devorworkstation(each includes the profiles before it in the chain). -
Apply them.
- If
~/code/profiledoes not exist, give the user the bootstrap command with the profiles. The user runs it in a terminal, because it can ask for thesudopassword. Wait until the user says it is done, and read its output. - If it exists, run
profile update, thenprofile install <profiles>. Whenprofileis not onPATHyet, run~/code/profile/scripts/profileinstead.
Check: each command exits with 0.
- If
-
Verify. Run
HOMEBREW_PROFILE_INSTALL_PROFILES="<profiles>" brew bundle check --no-upgrade --file ~/code/profile/brew/Brewfile. Ifdevorworkstationapplies, in a new login shell,mise lsshows the runtimes, andcommand -vofnode,python3,goandjavashows a mise path. Check: all these checks pass, or you told the user which check failed. -
Hand over the manual steps above. They need a browser or a password, so the user does them. Check: the user has the list.
The profile command updates this repository and installs the tools of the machine profiles. Run it from any directory.
profile update # pull the current branch, update the submodules, remove the ones the repo no longer lists
profile install # install the Homebrew packages of essentials
profile install dev # install the packages of dev and essentials, sync the nvim plugins, then the mise runtimes
profile install workstation # install the packages of workstation, dev and essentials, sync the nvim plugins, then the mise runtimes
profile cleanup # list what no profile needs, ask, then remove itprofile update removes a submodule that the repo no longer lists: its working tree, its directory under .git/modules/, and its section in .git/config. It asks no question. It keeps the submodule and prints a warning if the submodule has uncommitted changes, untracked files, a stash, or commits that no remote-tracking ref contains.
profile install stores no selection: each run applies only the profiles that you name. A profile always includes the profiles before it in the chain. After brew bundle, dev and workstation run :ProfileSync in a headless nvim and then mise install; essentials stops after brew bundle. It never removes packages.
profile cleanup has a Homebrew section and a mise section. Each section lists what it would remove, asks its own [y/N] question, and removes that list only if you answer y. You can remove one list and keep the other.
- The Homebrew section lists the formulae, casks and taps that no profile lists and asks, for example,
Remove these 6 Homebrew packages? [y/N]. It always compares against every profile, so it never removes a package of a profile. Before it asks, it checks each candidate against the entries of every profile, also by its old names and aliases, and prints a warning such askept docker: a profile lists itfor each candidate that it drops from the list. If it cannot read the names of a candidate, it keeps that candidate too. Onyit removes the list itself withbrew uninstallandbrew untap. It removes every installed version of a formula, and it turns Homebrew autoremove off, so a dependency that a profile lists stays. A dependency that no profile lists shows up in the next cleanup. It never touches uv tools, VS Code extensions or the otherbrew bundleextension types. The list also shows packages that you installed by hand and never added to the Brewfile, for example Slack or Zoom from Homebrew, so read it before you answer. A plainbrew bundlecommand with noHOMEBREW_PROFILE_INSTALL_PROFILESalso applies every profile. - The mise section lists the runtime versions that no config uses, one
tool@versionper line, frommise ls --prunable --json. This needsjq, which is inessentials. Then it asks before it runsmise prune. If mise cannot list the versions, for example because of a config error,profile cleanupasks nothing and exits with 1.
profile exits with 0 on success, 1 when a step failed, and 2 on a usage error. profile update does not run install.sh. Run install.sh again after an update that adds a link.
Both profile install and profile cleanup skip their Homebrew section when brew is not on PATH, and their mise section when mise is not on PATH. profile install skips the nvim plugin sync when nvim is not on PATH.
The nvim config is config/nvim, which install.sh links as a directory to ~/.config/nvim. The plugin versions are pinned in the committed config/nvim/lazy-lock.json.
The full setup — plugins, LSP, treesitter, options — is for dev and workstation machines. essentials keeps the neovim binary (the vim alias maps to it) but runs it stock: the config returns early when lazy.nvim was never provisioned. No file stores a profile selection; the provisioned lazy.nvim install is the signal.
profile install dev (and workstation) runs :ProfileSync in a headless nvim. It restores the plugins to the versions of the lockfile, removes the plugins that the config does not list, and installs the treesitter parsers. It exits with non-zero on failure, also when the loaded nvim config does not define :ProfileSync, and profile install then fails.
The Brewfile owns the language servers and formatters, all in dev: gopls, basedpyright, typescript, lua-language-server and stylua, plus tree-sitter-cli for the parsers. typescript is TypeScript 7, and nvim uses its native LSP (tsc --lsp). The config enables each server whose binary is on PATH. There is no Mason.
To update the plugins, run :Lazy update in nvim, test the result, and commit config/nvim/lazy-lock.json.
I've been using Oh-My-Zsh happily for many years and I could continue using it forever. I created this repository for several reasons.
- I realized that Oh-My-Zsh is basically a bunch of scripts that glue things together
- I realized that Oh-My-Zsh sometimes makes configuration decisions that are either not necessary or not to my liking
- Given 1 & 2, I realized that I can easily build something that does what I need and even takes me a step further, so that I don't have to make any manual adjustments after I install it
- Finally, I use this a lot to configure dev VM boxes, so it saves me a lot of time
With all that said, I still love, appreciate and recommend Oh-My-Zsh to most people. I haven't tried Zim yet, but it definitely looks good. So if you are not into fiddling with that kind of configuration be sure to check them out if you haven't done so yet.
You can, but you probably want to make some adjustments to make it your own. I would recommend to fork the repo and review the configuration, although it is very easy to make adjustments evern after installing, because it's all very very simple and magic free.
Pay extra attention to:
No, it is designed to work only with Zsh. Tested extensively on macOS with zsh versions:
- 5.7.1
- 5.8.1
- 5.9
The shell configuration also works on Linux. CI runs the test suite (make test) on ubuntu-latest and macos-latest. bootstrap.sh is the one macOS-only part.