Skip to content

Latest commit

 

History

20 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

dotfiles

Same shell setup on every machine. macOS, Linux, WSL.

curl -fsSL https://raw.githubusercontent.com/aligulzar729/dotfiles/main/bootstrap.sh | bash

or, if you would rather read it first:

git clone https://github.com/aligulzar729/dotfiles ~/Projects/dotfiles
cd ~/Projects/dotfiles
./install.sh

That 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 are

How it works

home/ 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.

What it installs

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

The dots command

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 / help

The usual loop:

dots brew                  # add a line, save
dots install packages      # apply
dots push                  # share it

Set DOTFILES_DIR in ~/.zshrc.local if you clone somewhere else.

SSH across devices

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

  1. Bitwarden → Settings → Allow SSH agent.
  2. Vault → New item → SSH key → Import, and paste an existing private key (~/.ssh/hub-devlab). Or generate a fresh one in Bitwarden and run ssh-copy-id -f -i <pubkey> user@server once per server.
  3. ./install.sh ssh, then log in for real: ssh myserver. Bitwarden pops a prompt to authorize the key. (ssh-add -l will not show it — IdentityAgent applies to ssh, not to a bare ssh-add.)
  4. 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.sh

When 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 Linux

Paste 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.

SSH into this machine

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, repeatable

It 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:

  1. 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.
  2. Writes /etc/ssh/sshd_config.d/01-dotfiles.conf: no passwords, no root login, only your user. sshd -t checks it and a rejected file is removed.
  3. Turns sshd on (Remote Login on macOS) and checks that port 22 listens.
  4. macOS only: no sleep on the charger, wake for network access, restart after a power cut.
  5. 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.

Layout

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.

Adding something

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 ...

Safety

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.

Windows

Use WSL2. The installer detects it, installs the whole CLI stack through Linux Homebrew, and skips the desktop apps.

wsl --install -d Ubuntu

Then inside WSL, same as any Linux box:

git clone <your-repo-url> ~/Projects/dotfiles
cd ~/Projects/dotfiles && ./install.sh --all

PowerShell, 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 copy home/.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.

Notes

  • 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 in skills.txt and installed with npx skills, so npx skills update pulls upstream fixes instead of this repo carrying a stale copy. impeccable and ui-ux-pro-max are neither, they are plugins from their own repos, carried by plugins.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.
  • $EDITOR is zed --wait when 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 ~/.zshrc is a symlink into this repo. Move what they added to ~/.zshrc.local and git 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.

SSH

  • Host config is shareable, private keys are not. Copy home/.ssh/config.example to ~/.ssh/config and add your hosts; it then works in terminal, git, rsync, and Termius alike.
  • .gitignore blocks id_*, *.pem, *.key etc. so a private key can never be committed by accident. *.pub and config.example stay tracked.
  • Keys themselves belong in a password-manager SSH agent (1Password, or the KeePassXC/Strongbox agent), not in this repo. Point IdentityAgent at its socket in your ssh config.

About

How I setup my machines

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages