Same shell setup on every machine. macOS, Linux, WSL.
curl -fsSL https://raw.githubusercontent.com/aligulzar729/dotfiles/main/bootstrap.sh | bashor, if you would rather read it first:
git clone https://github.com/aligulzar729/dotfiles ~/Projects/dotfiles
cd ~/Projects/dotfiles
./install.shThat opens a picker. Take everything on a new box, one step on an old one.
./install.sh --all # no prompts
./install.sh link claude # named steps
./install.sh --dry-run link # show what would change, change nothing
./install.sh --verbose zsh # stream output instead of logging it
./install.sh --list # what the steps arehome/ mirrors your home directory and every file in it gets symlinked into
place. ~/.zshrc is a link to home/.zshrc, so editing your shell config
edits the repo. Commit, push, pull on the other machine, done. No sync logic,
no copying.
Tools are installed from upstream: Homebrew formulae, the oh-my-zsh installer, each plugin's git repo, Zed's own install script. Nothing is copied between machines.
| Shell | zsh, oh-my-zsh, starship |
| History and nav | atuin, zoxide, fzf |
| Coreutils | eza, bat, ripgrep, fd |
| Extras | tealdeer, glow, yazi, ani-cli + ani-skip |
| Dev | git, gh, lazygit, git-delta, mailpit |
| Apps | Ghostty, Zed, Pika, VLC, Bitwarden, GitHub Desktop Plus, JetBrainsMono Nerd Font |
| Configs | zsh, git (delta), ssh (+ Bitwarden agent), starship, atuin, eza, ghostty, Zed |
| Claude Code | CLI, 6 marketplaces, 12 plugins, skills vendored here plus skills.txt |
| omz plugins | zsh-autosuggestions, zsh-syntax-highlighting, zsh-history-substring-search, zsh-completions, artisan |
Lives in home/.zshrc, so it exists wherever this repo lands. Tab completes.
On its own it cds to the repo, otherwise it opens whatever you need to edit.
dots brew # add a CLI tool dots install # run the installer
dots cask # add a macOS app dots link # re-link dotfiles only
dots zsh # shared shell config dots log # last install log
dots local # secrets, per-machine dots status # git status
dots claude # marketplaces/plugins dots diff # git diff
dots doctor # verify this machine dots lint # shellcheck
dots bench # time zsh startup dots backup # -> Bitwarden
dots vault # what is backed up dots restore # <- Bitwarden
dots audit # scan git history dots adopt # stray brews
dots servers # fleet health dots deploy # profile -> host
dots starship # prompt dots pull # git pull + re-link
dots ghostty # terminal dots push # add + commit + push
dots zed # Zed settings dots keymap # Zed keybindings
dots tasks # Zed tasks dots snippets # Zed snippets
dots ssh # shared ssh defaults dots hosts # your servers (local)
dots sshable # this machine -> reachable over SSH, see below
dots atuin / eza / steps / readme / helpThe usual loop:
dots brew # add a line, save
dots install packages # apply
dots push # share itSet DOTFILES_DIR in ~/.zshrc.local if you clone somewhere else.
One key, one host list, every machine. The key lives in Bitwarden, not on disk, so a new device needs no new key and no server-side change.
Once, on this machine
- Bitwarden → Settings → Allow SSH agent.
- Vault → New item → SSH key → Import, and paste an existing private key
(
~/.ssh/hub-devlab). Or generate a fresh one in Bitwarden and runssh-copy-id -f -i <pubkey> user@serveronce per server. ./install.sh ssh, then log in for real:ssh myserver. Bitwarden pops a prompt to authorize the key. (ssh-add -lwill not show it —IdentityAgentapplies tossh, not to a baressh-add.)- Delete the on-disk copies once a real login works.
On every other device
# 1. Bitwarden: install, log in, Settings > Allow SSH agent
# 2. dotfiles
git clone https://github.com/aligulzar729/dotfiles ~/Projects/dotfiles
cd ~/Projects/dotfiles && ./install.shWhen it reaches the ssh step and finds no server list, it stops and asks for
one, printing the exact command for both ends. On a machine that already has
the list:
pbcopy < ~/.ssh/config.d/10-servers.conf # wl-copy or xclip on LinuxPaste that into the Notes field of your Bitwarden SSH key item. On the new
machine, copy those Notes, paste at the prompt, press Ctrl-D.
It validates the paste with ssh -G and discards anything that would not parse,
so a mispaste cannot break ssh for every host. Ctrl-D on an empty prompt skips.
No key generation, nothing added to any server. The step also detects that machine's Bitwarden socket, which differs between App Store, .dmg and Linux builds.
Adding a server later: dots hosts, back the file up to the Bitwarden note
again, then re-paste on the other machines.
Where things live
| File | What | Synced by |
|---|---|---|
~/.ssh/config |
include lines only | created by the ssh step |
~/.ssh/config.d/00-agent.conf |
Bitwarden socket path | generated per machine |
~/.ssh/config.d/10-*.conf |
your real servers | pasted from a Bitwarden note |
~/.ssh/config.d/99-defaults.conf |
shared defaults | this repo, symlinked |
Real hostnames never enter this repo, it is public. ssh reads
config.d/*.conf in sorted order and is first-match-wins, hence the numbering:
agent first, hosts next, catch-all last.
known_hosts is deliberately not synced. It is a per-machine cache and it
leaks your server list; StrictHostKeyChecking accept-new removes the
first-connect prompt instead.
dots sshable <name> makes the current machine reachable over SSH from your
other devices, inside the same network or from anywhere, without opening a
port on any router. macOS, and Linux with apt, dnf, pacman, zypper or apk,
on systemd or OpenRC.
dots sshable workstation # this machine as "workstation"
dots sshable workstation --save # also add it to this machine's 10-servers.conf
dots sshable workstation --key "ssh-ed25519 AAAA… phone" # authorize an extra key, repeatableIt is not an install step: it needs sudo, and it opens a network service, so
./install.sh --all never runs it. Safe to re-run. In order, it:
- Adds the Bitwarden agent's public keys, and any
--key, to~/.ssh/authorized_keys. With no key at all it stops here, because the next step would lock everyone out. - Writes
/etc/ssh/sshd_config.d/01-dotfiles.conf: no passwords, no root login, only your user.sshd -tchecks it and a rejected file is removed. - Turns sshd on (Remote Login on macOS) and checks that port 22 listens.
- macOS only: no sleep on the charger, wake for network access, restart after a power cut.
- Installs Tailscale, waits for you to sign in, and names the machine
<name>on your tailnet.
At the end it prints the Host blocks to add everywhere else: <name> over
Tailscale, and <name>-lan over mDNS for the same network (macOS always,
Linux only when avahi runs). It never touches the Bitwarden note itself:
dots hosts, paste, dots backup, then dots restore on the other machines.
To connect, a device needs Tailscale signed in to the same account and your
Bitwarden key, then ssh <name>. A phone app such as Termius or Blink has its
own key: pass it with --key.
FileVault: after a reboot, SSH may not answer until someone logs in at the screen. Test one restart before relying on it.
Undo: delete /etc/ssh/sshd_config.d/01-dotfiles.conf, turn off Remote
Login (macOS) or sudo systemctl disable --now ssh (Linux), and log out of
Tailscale.
install.sh args, step picker, run, summary
lib/ui.sh colors, spinner, picker, summary
lib/core.sh platform detection, task runner, ensure_* helpers
lib/steps.sh loader: sources steps.d, builds the ordered step list
lib/steps.d/
system.sh prereqs, homebrew, packages, fonts
apps.sh Ghostty, Zed, GitHub Desktop Plus, Bitwarden
shell.sh oh-my-zsh + plugins, login shell
dotfiles.sh symlink home/ into ~
ssh.sh ssh config, Bitwarden agent socket, key permissions
claude.sh Claude Code CLI, skills, plugins
macos.sh macOS defaults: keyboard, Finder, Dock, screenshots
doctor.sh health check: verifies, never writes
lib/backup.sh private files to and from Bitwarden, see `dots backup`
lib/scan.awk the secret and PII patterns, shared by hook and audit
lib/servers.sh fleet health across 10-servers.conf
lib/deploy.sh copies remote/ onto a server
lib/sshable.sh makes this machine reachable over SSH, see `dots sshable`
lib/adopt.sh adds stray brews into the Brewfile
remote/ minimal server profile: inputrc, tmux, vim, aliases
bootstrap.sh clone plus install, for a machine with nothing on it
hooks/pre-commit scans staged changes for secrets and PII
.pii-allow regexes the commit scan must not report
Brewfile CLI stack, all platforms
Brewfile.macos casks and fonts
home/ mirrors ~, everything in here gets symlinked
claude/plugins.txt marketplace to plugin manifest
claude/skills.txt third-party skills, installed with the skills CLI
claude/skills/ skills vendored here, each linked into ~/.claude/skills
.shellcheckrc lint config, see `dots lint`
.github/workflows/ shellcheck plus a container smoke test per distro
Three layers, one job each. UI never installs, steps never print. Every action
goes through task in core.sh, which owns the spinner, the log, the counters
and the result line. A step is a list of task "label" some_function lines.
Steps live in lib/steps.d/ grouped by domain. Each file registers its steps
with register <name> <description>, so the description sits next to the code.
steps.sh sources them in dependency order and that order is the run order.
Adding a domain is a new file plus its name in the steps.sh source loop.
| Want to add | Edit | Apply |
|---|---|---|
| A CLI tool | dots brew |
dots install packages |
| A macOS app or font | dots cask |
dots install packages |
| An omz plugin | OMZ_PLUGINS in lib/steps.d/shell.sh and plugins=() in home/.zshrc |
dots install zsh |
| A dotfile | drop it in home/ at its path relative to ~ |
dots link |
| A Claude plugin | dots claude |
dots install claude |
| A step | in the matching lib/steps.d/*.sh: register, then a step_x function |
dots install x |
| A whole domain | new lib/steps.d/<name>.sh, add <name> to the loop in lib/steps.sh |
dots install ... |
Every step checks before it acts, so a second run reports unchanged instead
of redoing work. A real file in the way of a symlink is moved to <name>.bak,
never deleted. Secrets live in ~/.zshrc.local, which is created from the
example on first run and gitignored. One failing task does not abort the run;
failures are counted and the log path is printed at the end.
Use WSL2. The installer detects it, installs the whole CLI stack through Linux Homebrew, and skips the desktop apps.
wsl --install -d UbuntuThen inside WSL, same as any Linux box:
git clone <your-repo-url> ~/Projects/dotfiles
cd ~/Projects/dotfiles && ./install.sh --allPowerShell, cmd, Git Bash, MSYS2 and Cygwin do not work. This is bash, it relies on symlinks and a POSIX home, and Homebrew has no Windows build.
Desktop apps go on the Windows side:
- Ghostty has no official Windows build. Use Windows Terminal or WezTerm and run WSL inside it.
- Zed has a Windows build, check zed.dev/download
for its status. It reads config from
%APPDATA%\Zed, so copyhome/.config/zed/there by hand. - GitHub Desktop Plus, see releases.
Keep the repo on the Linux filesystem, not /mnt/c. Symlinks and permissions
misbehave across the Windows mount and this repo is all symlinks.
- Skills split two ways. Anything written here lives in
claude/skills/and is linked one by one into~/.claude/skills, so other skills already on the machine stay put; a real directory in the way is backed up to<name>.bak. Everything from someone else's repo is listed inskills.txtand installed withnpx skills, sonpx skills updatepulls upstream fixes instead of this repo carrying a stale copy.impeccableandui-ux-pro-maxare neither, they are plugins from their own repos, carried byplugins.txt. - Zed's config is symlinked file by file, so changing settings in the Zed UI
edits the repo. If an update ever replaces a symlink with a real file, run
dots link. - Zed's
prompts/is not versioned. It is an LMDB database with a lock file, git cannot merge it. $EDITORiszed --waitwhen Zed is on PATH. Override in~/.zshrc.local.- Installers that append to
~/.zshrc(Laravel Herd does this on every update) write into the tracked file, because~/.zshrcis a symlink into this repo. Move what they added to~/.zshrc.localandgit checkout home/.zshrc. - Linux gets Ghostty from the distro repo where one exists (Arch, Fedora), otherwise the installer prints the download URL.
- Needs bash 3.2, so it runs on stock macOS.
- Host config is shareable, private keys are not. Copy
home/.ssh/config.exampleto~/.ssh/configand add your hosts; it then works in terminal, git, rsync, and Termius alike. .gitignoreblocksid_*,*.pem,*.keyetc. so a private key can never be committed by accident.*.pubandconfig.examplestay tracked.- Keys themselves belong in a password-manager SSH agent (1Password, or the
KeePassXC/Strongbox agent), not in this repo. Point
IdentityAgentat its socket in your ssh config.