One command. Fresh machine to finished install:
curl -fsSL https://raw.githubusercontent.com/eduuh/dotfiles/main/bootstrap.sh | bash -s -- --workChoosing the flag is the only decision you make. Everything after it is automatic.
| Machine | Flag |
|---|---|
| Work laptop | --work |
| Personal machine | --personal |
| Both sides on one box | --work --personal |
| Toolchain only | (omit; run | bash with no -s --) |
The flag is forwarded through all three phases, so there is nothing to re-run afterwards with a different flag:
- bootstrap β installs
git/zsh, clones dotfiles into the bare + worktree layout (~/projects/bare/dotfiles.git+~/projects/worktree/dotfiles/main) - prep (attended β the only part that talks to you) β sudo, GitHub sign-in
- SSH key, profile
- setup (unattended β walk away) β packages, tools, stow, and every repo clone, including the work/personal lists
Symlinks are always stowed from the main worktree, so you can wt add feature/x
and edit dotfiles in multiple worktrees without disturbing them.
Sign in as the account that owns
personal-notes. Prep checks for it by name and signs it in if it is missing, because being logged in as some account is not enough β on a work machine gh is usually the work account, which cannot see a single private personal repo. gh keeps both accounts, so the work one stays usable.
Already cloned? Same flags, run the phases directly from the main worktree:
cd ~/projects/worktree/dotfiles/main
./prep.sh --work # chains into setup.sh, forwarding the flagprep.sh takes --profile, --work, --personal, and --prep-only (stop instead
of chaining into setup). Flags other than --profile are passed straight through.
setup.sh flags β you only need these when re-running a phase by hand:
| Flag | Effect |
|---|---|
--work |
Also install work-machine tools and clone the work repos (see Private repo lists) |
--personal |
Also clone the personal-only repos (see Private repo lists) |
--profile <core|dev|desktop> |
Override the profile recorded by prep.sh |
--force |
Re-run every step, ignoring the cached done-file |
--windows-admin |
(WSL) Also run win-dot's elevated run.ps1 β raises a UAC prompt (see The Windows side) |
--windows-keyboard |
(WSL) Also install the Keyflow keyboard layout on Windows |
reset |
Clear recorded step state and exit |
Python setup (venv + pynvim/requests, and on Ubuntu the python3.10 /
python3.10-venv apt install) is opt-in. Enable it when you need it:
SETUP_PYTHON=1 ./setup.shAuto-detects the platform (macOS, Ubuntu, Arch, Fedora, Codespaces) and runs the same setup.sh everywhere β only the package backend differs. Fedora uses dnf, or on atomic variants (validated on COSMIC Atomic) layers packages with rpm-ostree. Changes apply live via --apply-live when possible; anything that can't be applied live is layered into the next deployment and finalized on reboot (setup prints a reminder). Configs and symlinks are stowed via GNU Stow.
Re-runs are safe. setup.sh is resumable β completed tool-install steps are cached (~/.local/state/dotfiles/done) and skipped, while package installation always re-runs, so adding a package to the list and re-running ./setup.sh installs it without redoing everything. Use ./setup.sh --force to re-run every step from scratch, or ./setup.sh reset to clear the recorded state.
setup.sh already launches the project clone in the background, so there is nothing
to run after it. These stay available for retrying a phase on its own:
./setup-projects.sh --work # re-run just the repo clones (parallel); flags as above
./setup-rust.sh # install the Rust toolchainRepos that aren't in REGULAR_CLONE_REPOS (see
.bin/setup/regular-repos.zsh) land in the bare +
worktree layout (~/projects/bare, ~/projects/worktree) so wt and tat pick
them up automatically.
On macOS, all Homebrew packages are managed via a Brewfile.
On WSL, dotfiles configures Linux β but half the machine is Windows, and none of
what a Windows application reads (PowerShell profile, Windows Terminal, GlazeWM,
VS Code, and the .wslconfig that sizes the WSL VM) can live in the WSL
filesystem. That half is eduuh/win-dot, and
setup.sh installs it for you:
clone_reposclones win-dot onto the Windows filesystem, at/mnt/c/Users/<you>/projects/win-dot, and symlinks~/projects/win-dotat it sowt,tatandbnlist it like any other repo. That routing comes from naming it in bothREGULAR_CLONE_REPOSandWINDOWS_CLONE_REPOSβ either list alone silently puts the clone inside WSL, where Windows tooling can't reach it.- The
windows-sidestep hands.bin/setup/windows-side.ps1topowershell.exe, which installs Scoop and git if missing and then runs win-dot's ownscripts/install.ps1(packages + profile stubs) andscripts/setup-git.ps1(checks the clone out over$HOME, wiring thedotcommand). Idempotent, so it reconciles on every run.
Restart PowerShell afterwards to pick up the profile.
What is not automatic. win-dot's scripts/run.ps1 β Developer Mode and the
WSL/VirtualMachinePlatform Windows features β needs Administrator. It self-elevates
through UAC and then asks its own Y/N question, so it can't run in setup's
unattended phase, and reaching it from inside WSL means the features it enables are
already on. Run it deliberately if you need it:
./setup.sh --windows-admin # accept the UAC prompt, then answer YA win-dot clone on NTFS gets core.filemode false and core.autocrlf true, applied
on every run rather than only at clone time. Without the second one, Windows tooling
rewrites the checkout with CRLF, git reports every tracked file as modified, and
clone_repos' "unsaved changes" guard then refuses to pull the repo ever again.
personal-notes is private and everything else hangs off it β the stow tree it
overlays onto $HOME, the work/personal repo lists, and the work-tools
installer. So every setup.sh run clones or pulls it, up front and
synchronously, before anything that reads from it. It is not part of the
deferred background clone, which is skipped once its step is recorded.
It is cloned over HTTPS, authenticated through gh β not SSH. On a work machine
the only SSH key is usually the work GitHub account's, which cannot see the repo, so
an SSH clone fails with a misleading Repository not found. The same applies to every
private repo in PRIVATE_EDUUH_REPOS (.bin/setup/common.sh): each is rewritten to
HTTPS and cloned with a credential helper pinned to the owning account via
gh auth token -u <account>. Pinned, not switched β the work account stays gh-active
and usable, and work repos are matched by owner/name so they are never rerouted.
Prep signs that account in for you, so normally there is nothing to do. To do it by hand:
gh auth login # as the account that owns personal-notes
./setup.sh # clones it, then stows ~/projects/personal-notes/stow/homeIf the account is missing, setup does not guess: it fails the step with the account
it actually found and the command to fix it, rather than letting the clone die with
Repository not found.
If a file the stow tree owns already exists as a real file, it is moved aside to
<file>.bak-<timestamp> and the stow is retried, instead of GNU Stow aborting
the whole tree.
bn's notes store (~/projects/branch-notes) is a private repo, and it is cloned
before the bn step for a reason that bit a fresh VM: installing bn starts
bn serve, which creates that directory itself, and a directory that already
exists is not a clone. Cloned late, the notes would sit in a plain local folder
with no remote and never sync anywhere.
If the directory is already there without a .git (a machine set up before this
existed), setup adopts it: it clones alongside, moves the local files in β repo
version winning, the local copy kept as <name>.local-<timestamp> β and swaps
the clone into place.
The split is by visibility, not by category: a repo GitHub already shows the
world is named here, a private one never is. So this repo's clone list holds the
toolchain plus the public personal projects, while private repo names, URLs and
the per-machine rules for them live in personal-notes.
Personal and work repos are both opt-in regardless of where they're listed β a work machine shouldn't pull your side projects even when their names are public:
Pass them to the one-command install and they reach every phase. These forms are for re-running a phase by hand:
./setup.sh --work # work tools + work repos
./setup.sh --personal # personal-only repos
./setup.sh --work --personal # both
./setup-projects.sh --work # repos only (standalone; setup.sh forwards the flags)Each flag enables private hooks in personal-notes/scripts/, every one a no-op
when the file is absent:
| Script | Sourced by | Runs |
|---|---|---|
setup-work-tools.sh |
install_work_tools |
--work |
setup-work-repos.sh |
clone_repos |
--work |
setup-personal-repos.sh |
clone_repos |
--personal |
Both run only after personal-notes is on disk, since that's where they live. The
clone step records a marker per shape (projects, projects-personal,
projects-work, projects-personal-work), so a machine set up plain still runs
the clone the first time you add a flag β rather than skipping it as already done.
The scripts are sourced, so they have _clone_single_repo, _is_wsl,
detect_distro, and the REGULAR_CLONE_REPOS / WINDOWS_CLONE_REPOS lists in
scope:
# ~/projects/personal-notes/scripts/setup-work-repos.sh
WORK_REPOS=(
"git@github.com:my-org/repo-a.git"
"git@github.com:my-org/repo-b.git"
)
for r in "${WORK_REPOS[@]}"; do
_clone_single_repo "$r" &
done
waittatβ Tmux session picker (provided by thebnrepo, installed via itsinstall.sh)wtβ Git worktree manager for bare repos (wt clone,wt add,wt list,wt remove)bnβ Branch notes manager for per-branch task tracking; also owns the tmux config (an external setup repo, no longer a submodule)ssh-exportβ Copy SSH key setup script to clipboard for bootstrapping new environments- Tmux (
Ctrl+Spaceprefix) β keybinding reference now lives in the bn repo:config/workflow/docs/tmux.md
Setup auto-detects Codespaces ($CODESPACES=true) and adjusts:
- Skips changing default shell to zsh
- Skips Rust, PNPM, Python venv, Starship installs
- Clones
personal-notesover HTTPS viagh(rungh auth loginfirst β no SSH key needed)
| Config | Details |
|---|---|
| Shell | zsh, starship, zoxide, fzf |
| Tmux | Sessions, plugins (resurrect + continuum) |
| Terminals | Kitty, Alacritty |
| macOS | AeroSpace, SketchyBar, Kanata (Colemak), skhd |
| Keyboard | Karabiner, Kanata |