Project

dot

, , , , , ,

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/bin and ~/.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 desktop workstation or a lean server baseline, 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 all only downloads what actually changed.
  • Environment checksdot init finds 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

CommandWhat it does
dot initCheck 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 listList all modules and profiles
dot project initDetect 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 exposing module_install, module_update and module_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, like configs/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

ProfileModules
desktopzsh, neovim, tmux, fzf, ripgrep, delta, gh, lazygit, nvm, uv, posting, claude
serverzsh, 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.

← all projects