Setting Up a New Machine - uhop/dotfiles GitHub Wiki
One-time steps to bring a vanilla machine into this project. Once set up, day-to-day usage is covered by the Workflows pages.
If you can get a shell on the new machine (physically or via SSH) and it has network access, the fastest path is the authoritative bootstrap script hosted in this repo:
curl -fsSL https://raw.githubusercontent.com/uhop/dotfiles/main/install.sh | shAdd --yes for unattended / CI use (no prompts):
curl -fsSL https://raw.githubusercontent.com/uhop/dotfiles/main/install.sh | sh -s -- --yesThe script installs OS prerequisites (curl + git + bash-4 + build tools), Homebrew, chezmoi, then runs chezmoi init --apply uhop/dotfiles. Requires an account with passwordless sudo (or root). Takes 5โ15 minutes depending on network. Safe to re-run.
If you already have a machine with this project, bootstrap-dotfiles is a thin wrapper that pipes install.sh over SSH and runs it unattended on the target:
# 1. Set up SSH access (dry-run first, then --apply)
bootstrap-remote newhost
bootstrap-remote newhost --apply
# 2. SSH + curl-pipe in one command
bootstrap-dotfiles newhostTo bootstrap the local machine and pull secrets from your jot bundle in one go:
# Pre-stage ~/.env + ~/.ssh/age.key from another machine or backup, then:
bootstrap-dotfiles --from-jot my-bundle-prefixThe rest of this page documents what happens under the hood and what to do when the automated paths don't work.
Passwordless SSH is a prerequisite for remote workflows and playbash. This project uses key-based authentication (public/private key pairs, not CA-signed SSH certificates).
bootstrap-remote automates key distribution and hardening (see below). To do it manually:
If you don't have one:
# Ed25519 (preferred โ short, fast, modern)
ssh-keygen -t ed25519 -C "[email protected]"
# RSA 4096 (fallback for older systems that don't support Ed25519)
ssh-keygen -t rsa -b 4096 -C "[email protected]"Accept the default location (~/.ssh/id_ed25519 or ~/.ssh/id_rsa). Use a passphrase for interactive machines; skip it for dedicated automation hosts where an agent can't prompt.
On macOS, add the key to the system keychain so it persists across reboots:
ssh-add --apple-use-keychain ~/.ssh/id_ed25519Then add to ~/.ssh/config (inside config.d/ โ see SSH client configuration):
Host *
AddKeysToAgent yes
UseKeychain yes
On Linux, this project installs ssh-agent as a systemd user service (see After setup: what's running below and Shell Environment ยง SSH agent (Linux)). Once dotfiles are applied, the agent starts automatically at login โ no eval "$(ssh-agent -s)" needed. Add keys with:
ssh-add ~/.ssh/id_ed25519Keys stay loaded until logout.
Copy your public key to each host you want to access:
ssh-copy-id user@hostThis appends your public key to ~/.ssh/authorized_keys on the remote host. You need password access for this one-time step.
This project also auto-populates ~/.ssh/authorized_keys on every chezmoi-managed machine by pulling your public keys from GitHub (via the GITHUB_USERNAME chezmoi variable). So once a machine has chezmoi, any key you've uploaded to GitHub grants access automatically.
To use the same identity from multiple machines (e.g., laptop + desktop), copy the private key:
# From machine A to machine B
scp ~/.ssh/id_ed25519 ~/.ssh/id_ed25519.pub user@machineB:~/.ssh/Then set permissions on machine B:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pubAlternatively, generate a separate key pair on each machine and add all public keys to GitHub โ chezmoi will distribute them to authorized_keys on every managed host.
Files like ~/.env, ~/.msmtprc, and ~/.ssh/age.key contain secrets that can't live in git. When machines can reach each other, scp or playbash put works. When they can't, use jot as a secure intermediary โ files are encrypted and stored on S3.
Once the encryption key and AWS credentials (~/.env) are on the new machine, pull everything else with jot get:
jot get sys/msmtprc && mv msmtprc ~/.msmtprc && chmod 600 ~/.msmtprcSee Jot โ Sharing sensitive files for naming conventions and the full workflow.
After key-based access is working, disable password authentication and root login on each remote host. Test your key access first โ getting this wrong locks you out.
# Verify key access works BEFORE changing anything
ssh user@host "echo ok"Then on the remote host:
# Edit sshd_config (use sudoedit if available)
sudoedit /etc/ssh/sshd_configChange or add these lines:
PasswordAuthentication no
PermitRootLogin no
Restart the SSH daemon:
# Linux (systemd)
sudo systemctl restart sshd
# macOS
sudo launchctl stop com.openssh.sshd
# (macOS restarts it automatically)Verify by trying to connect with a password from another terminal (it should be refused) while keeping your existing session open as a safety net.
- Key-based login works for your user
-
PasswordAuthentication noin/etc/ssh/sshd_config -
PermitRootLogin noin/etc/ssh/sshd_config -
sshdrestarted - Password login rejected (tested from a separate session)
- Other users who need access have their keys in
authorized_keys
This project deploys ~/.ssh/config with:
Include config.d/*
Host *
ControlMaster auto
ControlPath ~/.ssh/sockets/%r@%h-%p
ControlPersist 5m
Connection multiplexing is on by default: the first connection to a host opens a control socket; subsequent connections reuse it. This speeds up playbash fan-out (no repeated handshakes) and makes scp/sftp after ssht instant.
Add host-specific settings in ~/.ssh/config.d/ (one file per host or group):
# ~/.ssh/config.d/servers
Host srv1
HostName 192.168.1.10
User eugene
IdentityFile ~/.ssh/id_ed25519
Host srv2
HostName 192.168.1.11
User eugene
Host *.internal
ProxyJump bastion
These host aliases are used directly by ssht, playbash, and all other SSH tools. The playbash inventory references the same host names โ SSH-level overrides (user, port, key, proxy) belong here, not in the inventory.
If you have an existing machine with this project installed, use bootstrap-dotfiles to prepare the new host:
bootstrap-dotfiles newhostThis detects the remote OS, generates a tailored install script, and uploads it. Then follow the printed instructions โ SSH into the new host and run the script:
ssht newhost
/tmp/install-dotfiles.shThe script handles prerequisites, brew, and chezmoi init --apply. It will ask for sudo password and chezmoi configuration (name, email, GitHub username, GUI). At the end it offers to reboot.
When the new machine can't reach an existing operator machine over SSH (different network, NAT, behind a firewall), use --from-jot directly on the new host. It pulls system files from S3 via jot-deploy instead of copying them from the operator.
# On the new machine (after ~/.env and ~/.ssh/age.key arrive out-of-band):
bootstrap-dotfiles --from-jot sys/myhostPre-flight requires:
-
~/.envโ AWS credentials (S3 access). -
~/.ssh/age.key(or any of~/.ssh/id_ed25519,~/.ssh/id_rsa,~/.ssh/id_ecdsa) โ decryption key for the bundle.
These two files are the irreducible "chicken-and-egg" โ they must arrive on the new machine via USB, a password manager, or scp from a reachable host before --from-jot can do anything. See Jot ยง Bootstrap sequence.
Once the script runs:
- Installs prerequisites + Homebrew.
- Installs chezmoi and runs
chezmoi init --apply. - Runs
jot-deploy <prefix>(report-only first), then prompts to--apply. - Offers to reboot.
See the README for manual steps. Summary for Ubuntu/Debian:
sudo apt install build-essential curl git git-gui gitk micro
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
brew install chezmoi
chezmoi init --apply --recurse-submodules=false uhopFor Red Hat-like distros (Fedora, RHEL, CentOS, Rocky, Alma):
sudo dnf install gcc gcc-c++ make curl git git-gui gitk unzip
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
brew install chezmoi
chezmoi init --apply --recurse-submodules=false uhopReboot after chezmoi init --apply. The chezmoi run scripts install system packages (via apt or dnf), brew packages, nvm, bun, tmux plugins, and write /etc/sudoers.d/chezmoi for passwordless maintenance commands. doas is not installed by default; install opendoas manually if you prefer it โ see Application Notes.
See Platform Notes for additional Ubuntu post-install steps (fonts, video codecs, clipboard tools) and macOS specifics (switching the default shell to bash). For per-distro support tiers, known quirks, and troubleshooting when the install hangs or fails, see Distro Compatibility.
A glance-able overview of what lands on a machine after chezmoi init --apply finishes โ so there are no surprises. Each row links to the file or wiki page with full details.
| Service | Scope | Purpose | Source |
|---|---|---|---|
ssh-agent.service + .socket
|
user (systemd) | Socket-activated ssh-agent; one per login, listens at $XDG_RUNTIME_DIR/ssh-agent.socket. Overrides any distro-shipped unit so systemd user env publishes the right SSH_AUTH_SOCK. See Shell Environment ยง SSH agent (Linux). |
dot_config/systemd/user/ssh-agent.{service,socket} |
[email protected] |
user (systemd) | Template unit used by periodic task notification wrappers โ emails journalctl context on unit failure. Instantiated per failing unit, not always-on. |
dot_config/systemd/user/[email protected] |
| Periodic tasks you add | user (systemd) / launchd |
setup-periodic add ... creates timers for daily/weekly maintenance (playbash run @self daily --report etc.). None installed by default โ you opt in per host. See Workflows: maintenance ยง Scheduled maintenance. |
created on demand |
On macOS the systemd units are not deployed (.chezmoiignore excludes .config/systemd on non-Linux). ssh-agent is provided by the OS Keychain / launchd integration; see SSH key setup above.
| What | Where | Why |
|---|---|---|
Passwordless sudo rules |
/etc/sudoers.d/chezmoi |
Lets maintenance commands (upd, dcm, restart docker/containerd, etc.) run without a password prompt. Platform-specific command set. |
doas config (if installed) |
/etc/doas.conf |
Same idea via doas; aliased to sudo in shells. Opt-in: install opendoas to activate. See Application Notes ยง passwordless privilege escalation. |
SSH authorized_keys
|
~/.ssh/authorized_keys |
Rendered from GitHub-registered keys for GITHUB_USERNAME. Any key you upload to GitHub grants access to every chezmoi-managed machine. |
| SSH client config |
~/.ssh/config, ~/.ssh/config.d/
|
Connection multiplexing by default; per-host overrides go in config.d/. See SSH client configuration. |
etckeeper (if installed) |
/etc/.git |
Tracks /etc in git. Commits happen on package install. |
| What | Where | Notes |
|---|---|---|
| Shell environment |
~/.bashrc, ~/.bash_aliases
|
Exports (EDITOR, FORCE_COLOR, GITHUB_USERNAME, JOT_AWS_S3_BUCKET), PATH, tool init, aliases. Full map: Shell Environment. |
| Per-machine overrides | ~/.env |
User-owned, untracked. Secrets and per-machine values. See Shell Environment ยง .env loading. |
| Custom CLI utilities | ~/.local/bin/ |
Every tool in Utilities plus their bash completions. |
| Package set | brew + apt/dnf
|
Installed by run_onchange_before_install-packages.sh.tmpl (system) and run_once_after_install-*.sh.tmpl (brew, nvm, bun, tmux plugins, options.bash). |
By default, nothing on this dotfiles setup opens a listening port. Any listeners are from apps you install separately (Docker Compose stacks, ttyd if you run it, etc.) โ not from chezmoi scripts.
-
ssh-agent user service:
systemctl --user disable --now ssh-agent.socket ssh-agent.service. Remove theSSH_AUTH_SOCKexport by editingdot_bashrc.tmpl(the{{ if eq .osFamily "linux" }}block) and re-applying. -
Periodic tasks:
setup-periodic list, thensetup-periodic remove <name>. -
Passwordless sudo:
sudo rm /etc/sudoers.d/chezmoi(will be regenerated on nextchezmoi applyof the relevant script โ remove therun_onchange_after_install-sudoers.sh.tmplfrom apply via.chezmoiignoreif permanent).
Settings the dotfiles leave to you, by choice:
-
A Mac that other machines connect to (
ssh,playbash): turn off system sleep. Otherwise the Mac sleeps most of the day, and connections to it time out. For details, see Platform Notes ยง macOS.sudo pmset -a sleep 0
After reboot, verify the setup:
- Shell prompt shows git branch info (bash-git-prompt is working)
-
fastfetchruns on terminal open -
eza,bat,fzf,zoxideare available (ls,cat, Ctrl+R,z) -
node --versionreturns a current version (nvm is working) -
microopens (editor is working) -
git guiandgitkopen (GUI git tools, if the machine has a GUI) -
tmuxstarts; install plugins with Prefix+I on first run -
playbash hostslists your inventory (if applicable) - On Linux:
systemctl --user status ssh-agentisactive;echo $SSH_AUTH_SOCKresolves under/run/user/<uid>/