dot is my portable dotfiles CLI. One command turns a fresh laptop or an empty server into my machine: the same shell, the same editor, the same tools, the same keybindings.
It’s written in pure zsh, installs everything into ~/.local without sudo, and runs on both macOS and Linux. It also keeps my whole dotfiles configuration in one repository, so there’s a single place to change things and a single place to pull them from.
Why I built it
Every new device used to cost me an afternoon. Install zsh, remember which plugins I use, copy over a Neovim config that had drifted between machines, find the right tmux build, and discover halfway through that I don’t have sudo on this particular server.
Existing dotfile managers mostly solve the “copy the config files” half. I wanted something that also installs the tools themselves, works on a locked-down box, and keeps every machine on the same versions. So dot does both.
What it does
- No sudo required — tools land in
~/.local/binand~/.local/opt, so it works on shared servers and managed machines. - macOS and Linux — one codebase, with OS detection for downloads and package names.
- Profiles for different machines — a full
desktopworkstation or a leanserverbaseline, each a single command. - Configs in one place — zsh, Neovim, lazygit and shell aliases live in the repo and are symlinked into place, so an edit is live everywhere after a pull.
- Cheap updates — HTTP ETags mean
dot update allonly downloads what actually changed. - Environment checks —
dot initfinds what’s missing (zsh, curl, git, a compiler,PATH) and offers to fix it.
Quick start
On a machine that doesn’t even have zsh yet, a tiny POSIX sh bootstrap installs it first (via the package manager, or built from source into ~/.local as the sudo-free fallback) and then hands over to dot init:
terminal
git clone https://github.com/MikeWhileCoding/dot.git ~/.config/dot
~/.config/dot/bootstrap.sh # add --yes to skip the prompts
After that, setting up a machine is one line:
terminal
dot install --profile desktop # full workstation
dot install --profile server # lean server baseline
dot status # what's installed, and which version
dot update all # pull in anything that changed
Commands
| Command | What it does |
|---|---|
dot init | Check the environment (zsh, build tools, PATH) |
dot install <module> | Install a single tool |
dot install --profile <name> | Install every module in a profile |
dot update [<module>|all] | Update one tool or everything |
dot status [<module>] | Show installed version and update stamp |
dot config [<module>|all] | Re-apply config symlinks and settings |
dot list | List all modules and profiles |
dot project init | Detect a project’s containers and configure editor tooling for it |
How it works
dot is deliberately boring inside. There’s no framework and no dependencies beyond zsh, curl and tar — just a few conventions:
- Modules are zsh files in
modules/, one per tool, each exposingmodule_install,module_updateandmodule_status. - Profiles are zsh files in
profiles/that list which modules belong together, with an optional post-install hook. - Configs live in
configs/<tool>/and are symlinked to their usual locations, likeconfigs/nvim/→~/.config/nvim. - Update checks store the remote ETag in
~/.local/share/dot/on install; an update only downloads when the ETag has changed.
Adding a tool means adding one file:
modules/mymodule.sh
MODULE_NAME="mymodule"
MODULE_DESC="Short description"
module_install() {
# download, extract to ~/.local/opt, link into ~/.local/bin
}
module_update() {
# use needs_update for the ETag check
}
module_status() {
# print version / stamp info
}
Why pure zsh
A dotfiles tool has to run before anything else is installed. Writing it in the shell I was going to install anyway means there’s nothing to bootstrap except zsh itself — and the bootstrap script handles that too.
Profiles and modules
| Profile | Modules |
|---|---|
desktop | zsh, neovim, tmux, fzf, ripgrep, delta, gh, lazygit, nvm, uv, posting, claude |
server | zsh, neovim, tmux, fzf, ripgrep, delta |
Other modules include Intelephense (with a guided licence setup that keeps the key out of the repo) and a set of shell aliases.
The configuration it carries
Shell
A managed ~/.zshrc with oh-my-zsh and the git, GitHub and Docker plugins. Anything machine-specific goes in ~/.zshrc.local, which stays out of version control.
Neovim
A lazy.nvim setup built around my daily work: telescope, oil.nvim and harpoon for navigation, native LSP with mason, treesitter, conform and nvim-lint for formatting and linting, nvim-dap with Xdebug for debugging, and optional Copilot autofill and Claude Code integration that can be switched off per buffer or entirely.
PHP and Laravel in Docker
Most of my projects run inside containers, which usually breaks editor tooling. dot project init detects Sail, a running container that mounts the project, or a compose file, and writes a small .nvim-tools.json. From then on pint, phpstan, artisan, biome and the debugger run inside the right container, with paths translated between host and container. dot project check verifies it actually works.
lazygit
A theme plus a few custom commands for repos with pre-commit hooks, whose output lazygit would otherwise cut off.
What I took away
- Design for the worst machine. Assuming no sudo and no zsh made dot work everywhere, including the easy machines.
- Conventions beat configuration. Three functions per module is enough structure to add a tool in minutes.
- Symlinks keep configs honest. Because the live config is the repo, machines can’t quietly drift apart.
- Build it for yourself first. dot is opinionated and tuned to my setup — and that’s exactly why I use it every day.
Built for my setup
dot is public so you can read it, borrow from it or fork it, but it’s not a general-purpose product: some of the configuration is very specific to how I work.
