Highlights
The roles reference lists every alias, function, and variable. This page is the opposite: a short tour of the moments that make a Dotfiles Pro machine a pleasure to use day to day, grouped by what you are doing. Each one links to the role that ships it, so you can turn it on by adding the role to your playbook.
Set up and keep a machine up-to-date
One command sets up a whole machine
That command is dotpro apply: it reads your repo, works out what the machine should look like, and converges it — the same command for the first setup and every update. The installer just gets you there, provisioning Ansible, cloning your repo, and installing the collection:
curl -LsSf https://dotfiles.pro/install.sh | sh -s -- git@gitlab.com:yourname/mydotfiles.gitThen, now and next week:
dotpro applyBecause every role is idempotent, re-running only touches what changed. There is no separate "first install" versus "update" story to remember (see Getting started).
Bootstrap a machine that has no SSH key yet
The chicken-and-egg of a brand-new machine: cloning your private repo needs an SSH key the machine does not have yet. install.sh handles it.
A failed git@ clone falls back to HTTPS and flips origin back to SSH for future pulls, and when even that is not possible it generates an ed25519 key, prints the public half, and pauses while you register it with your git host before retrying. A fresh box clones itself without a manual key dance.
Keep a machine up-to-date on a schedule
Every starter ships a systemd user timer, so a machine re-applies itself on a cadence and never quietly drifts. Enable it once and the run pulls the latest collection and applies your playbook, limited to the current host when the repo carries an inventory.
$ systemctl --user enable --now dotfilespro-updater.timerThe timer runs daily with Persistent=true, so a run missed while the machine was off fires at next boot, and a randomized delay keeps a fleet from stampeding at the same minute. Unattended machines stay up-to-date on their own.
Re-apply just your settings in seconds
Changed one config value and want it live without re-checking every package? The dotpro role's dotpro tags config re-runs only the configuration tasks, and dotpro tags git targets a single role. The full update is dotpro apply. Tight feedback loops make tweaking your setup something you actually do, rather than something you batch up and dread.
dotpro is a smart Ansible wrapper
Every dotpro command shells out to plain Ansible, so the raw command is always one step away:
dotpro apply # ansible-playbook playbook.yaml --ask-become-pass
dotpro plan # ansible-playbook playbook.yaml --check --diff
dotpro tags git # ansible-playbook playbook.yaml --tags git
dotpro tags # ansible-playbook playbook.yaml --list-tags
dotpro vars # ansible-inventory --host $(hostname) --yaml
dotpro requirements --upgrade # ansible-galaxy collection install -r requirements.yml --upgradeThe example ansible.cfg turns diffs on for every run and hides ok and skipped tasks, so an apply shows only what actually changed. Here a one-line truncation_length tweak to the Starship config is the single change; every package-install task is skipped by the config tag:
$ dotpro tags config
PLAY [Configure this machine] **************************************************
TASK [starship : Render ~/.config/starship.toml] *******************************
--- before: /home/ada/.config/starship.toml
+++ after: /home/ada/.config/starship.toml
@@ -19,7 +19,7 @@
[directory]
-truncation_length = 4
+truncation_length = 8
truncate_to_repo = true
changed: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=9 changed=1 unreachable=0 failed=0 skipped=27 rescued=0 ignored=0The skipped count in the recap is every package-install task the config tag stepped over, so the run finishes in a couple of seconds instead of re-checking every tool.
Know exactly what an update changed
After an apply bumps tool versions, upgrades prints the log of what moved, with every tool a run changed grouped under that run's timestamp, and versions shows a table of every installed tool and its current version with the most recent batch on top. No guessing whether that kubectl behaviour change is new. From the unix role.
$ upgrades
[2026-05-28 09:11]
curl 8.5.0 -> 8.7.1 (new)
[2026-06-10 14:32]
git 2.42.0 -> 2.43.0
kubectl 1.29.2 -> 1.30.0Every run is on the record
The starter ansible.cfg sets log_path = ~/.ansible/audit.log, so every apply appends a full record of which tasks ran and what changed. On your own machine that is a handy history; when you provision a teammate's, it is a paper trail you can point to. Nothing hides behind a one-shot curl | bash: the playbook lives in your repo and every run is logged.
$ tail -n 3 ~/.ansible/audit.log
2026-06-18 09:03:11,481 p=12044 u=ada | TASK [starship : Render ~/.config/starship.toml]
2026-06-18 09:03:11,733 p=12044 u=ada | changed: [localhost]
2026-06-18 09:03:14,002 p=12044 u=ada | PLAY RECAP localhost : ok=9 changed=1 failed=0An agent-ready machine from the first apply
Declare your projects and skill sources once and an apply leaves the machine ready for both you and your coding agents: the git role clones every project, the projects role gives you jump commands into them, the agents role writes the ~/.agents inventories (TOOLING.md, PROJECTS.md, SKILLS.md) agents read instead of probing, and the claude role materializes the selected skills for Claude Code. This session runs on a machine right after its first apply (recorded by roles/agents/record-demo.sh, so the demo is reproducible):
Everyday shell
A shell framework you never edit by hand
Every role drops its aliases, functions, and environment into ~/.profile.d/ rather than appending to .bashrc, and ~/.profile sources those fragments in order at login. Adding or removing a role cleanly adds or removes its shell setup, with nothing left orphaned in a dotfile you have to prune later.
The login banner, rebrandable with unix_init_header, lists each fragment as it loads, so a slow or broken startup is easy to spot. From the unix role.
┏━━━━━━━━━━━━━━━━━━┓
┃ dotfiles.pro ┃
┗━━━━━━━━━━━━━━━━━━┛
Loading: ~/.profile.d/aliases.sh
Loading: ~/.profile.d/environment.sh
Loading: ~/.profile.d/ssh.shOne alias per project that sets the whole scene
Define a project function once, then give each repo a one-line alias. Typing the alias changes into the directory, sets the terminal tab title, prints the remote, and shows git status. Chain context setup ahead of it and a single word selects a cloud profile and a cluster too, so you land in a repo fully oriented. Built on the unix role's tab, header, and kv helpers.
functions:
project: |
tab "${1:-$(basename "$PWD")}"
header "Project $(basename "$PWD")"
kv Location "$PWD"
kv Repository "$(gitrepo)"
header 'Git status'
git status
aliases:
production: "digitalocean acme && kubernetes acme"
cloud: "cd /projects/acme/cloud/ && production && project 'Acme Cloud'"Now cloud drops you in the repo with the DigitalOcean context and Kubernetes cluster already selected, the tab renamed, and git status on screen.
Discover every command you have
You will never memorise every alias, and you do not have to. aliases lists every alias on the machine, grouped by the role that installed it; pass a role name to filter (aliases git). The listing shows each alias next to the command it runs, so it doubles as a cheat sheet, and commands lists the functions too. Also from the unix role.
$ aliases git
status: header 'Git status' && git status
log: header 'Git log' && git log --oneline --graph --decorate
gss: git-statusesEveryday git
Clone every project on a fresh machine
List the repositories you work in under git_repositories and an apply clones each one into place, so a new laptop arrives with every project already checked out under ~/projects/. Put shared repos in group_vars/all and per-machine repos in host_vars so each machine gets exactly the set it needs. Clones go over SSH and an existing checkout is left untouched, so re-running never disturbs local work. From the git role.
git_repositories_path: ~/projects/ # default: ~/
git_repositories: # default: the machine's `projects` inventory
api: git@gitlab.com:acme/api.git
web: git@gitlab.com:acme/web.gitSee every repo's status at once
gss walks every git repo under a directory and tells you, in one screen, which working trees are dirty, which branches are ahead of their remote, and which have stashes waiting. No more cd-ing through ten projects on a Monday morning wondering what you left unpushed on Friday. It ships with the git role.
$ gss ~/projects
Scanning: /home/ada/projects
━━━ dotfiles /home/ada/projects/dotfiles
✔ Working tree clean
✔ main (current) — up to date with origin/main
━━━ api /home/ada/projects/api
✘ Uncommitted changes:
M src/server.py
⚑ 1 stashed change(s)
✘ feature/auth (current) — 2 commit(s) ahead of origin/feature/auth (not pushed)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Summary
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Repositories found : 2
Dirty working trees : 1
Repos with unpushed : 1
Repos with stashes : 1Get to know a codebase in one command
Joining a new project? git-wtf runs five history queries at once and prints a profile of the repo: who commits most (who), the files that change most often (what), commit volume by year (when), the files that show up most in bug and fix commits (bugs), and the recent emergency reverts and rollbacks (hotfixes). It is a fast way to find the load-bearing, fragile parts of an unfamiliar codebase. Also from the git role.
Per-directory git identity, no manual switching
Commit to a work repo with your work email and a signing key, commit to a personal repo with your personal one, and never run git config user.email by hand again. The git role wires git's own includeIf so the right identity applies based on where the repo lives.
git_configs:
work:
match: gitdir:~/work/ # trailing slash matches everything under it
config:
user:
email: ada@work.example.com
signingkey: ABCD1234The same mechanism scopes config to a single repository or to a branch pattern, so release branches can require signed commits while everything else stays as-is.
Git defaults you would otherwise set by hand
git ships the global settings most people eventually configure manually, already on: pull.rebase for a linear history, push.autoSetupRemote so the first push of a branch needs no -u, rerere to replay conflict resolutions you have already worked out once, and an undo alias that rolls back the last commit while keeping the changes staged.
$ git undo # reset HEAD~1 --mixed, your work stays putSee the full set of shipped defaults for every key and the rendered ~/.gitconfig.
Cloud and Kubernetes context
Switch clusters and namespaces without juggling files
Drop per-cluster kubeconfigs into ~/.kube/configs/, then kube-config <name> to switch and kube-namespace <ns> to pin a namespace. From then on, the kubernetes role's kubectl wrapper injects --namespace into every call automatically once a namespace is set, so you stop typing -n my-namespace on every command and stop accidentally acting on default.
$ kube-config staging
$ kube-namespace payments
$ pods # already scoped to the payments namespace in staging
$ logsf api-7f9 # follow logs, still scoped, no -n neededThere are 25+ aliases for the operations you reach for most: pods, nodes, events, podsclean (delete every failed pod across namespaces), and podstopcpu to find the noisy neighbour.
A gcloud account that stays put
Juggling several Google Cloud accounts? Switch with gcp-account, gcp-project, and gcp-region, which set the CLOUDSDK_* variables for the current shell. The googlecloud role pins gcloud's active configuration to default, so a stray gcloud config set in one terminal never bleeds into another. Each shell holds exactly the account, project, and region you put there, and the prompt reflects it.
$ gcp-account ada@acme.com acme-prod europe-west1
$ gcp-project # list projects, or switch with: gcp-project other-projectA terminal per task, no crossed wires
Selection of cloud account, project, Kubernetes cluster, and namespace lives in environment variables scoped to each shell, not in a global config file every terminal shares. So you can keep one window on staging payments, another on production default, and a third authenticated as a different cloud account entirely, and switching context in one never moves the others. The googlecloud, kubernetes, and digitalocean roles all switch by exporting variables (CLOUDSDK_CORE_*, KUBECONFIG, KUBECTL_NAMESPACE, HELM_NAMESPACE, DIGITALOCEAN_CONTEXT) instead of writing back to active_config, the kubeconfig's current-context, or doctl's context key.
# terminal 1 # terminal 2, at the same time
$ gcp-account ada@acme prod $ gcp-account ada@personal sandbox
$ kube-config prod && namespace api $ kube-config sandbox && namespace web
$ pods # prod / api $ pods # sandbox / web, totally unaffectedThis is what makes the machine safe for coding agents like Claude Code, which run their own shells: an agent can switch account and cluster in its session without ever touching yours, or another agent's. The full mechanism is the per-shell context isolation concept.
Preview a Helm upgrade before you apply it
The kubernetes role installs Helm with the helm-diff plugin and keeps it up to date for you, no manual helm plugin install step. So before a release you can see exactly which objects an upgrade would add, change, or remove, the same preview-then-apply loop the rest of Dotfiles Pro leans on. k9s is installed alongside for a full terminal UI when you would rather browse than type.
$ helm diff upgrade api ./chart # shows the rendered changes, applies nothing
$ helm upgrade api ./chart # apply once the diff looks right
$ k9s # or browse the cluster in a terminal UIA prompt that always tells you where you are
The starship prompt is configured to surface the context that actually causes mistakes: the active Kubernetes context and namespace, the gcloud account, project, and region, the AWS profile, the Terraform workspace, the Docker and Helm context, plus git status and how long the last command took. The "am I about to apply this to production?" question answers itself from the prompt.
This session shows the prompt reacting to the real directory and git state as it changes (recorded by roles/starship/record-demo.sh, so the demo is reproducible):
ada on ⛵ gke-prod (payments) on ☁️ ada@acme (acme-prod) (europe-west1) ~/work/api on 👾 production took 1.2sEvery segment is driven by an environment variable the matching role's helper sets, so the prompt and your actual context never drift apart. Those same variables are what keep each shell isolated, so two terminals on different accounts or clusters show different prompts at once (see per-shell context isolation).
WSL
Reach the Windows side from the WSL shell
wsl's aliases close the gap between WSL and Windows. open launches a file or URL in its Windows app, copy pipes terminal output into the Windows clipboard, and desktop, downloads, and documents jump straight to those Windows folders without typing the /mnt/c/Users/... path.
$ cat report.csv | copy # now paste it into a Windows app
$ open ./diagram.png # opens in the default Windows viewer
$ downloads # cd to the Windows Downloads folderLens on Windows, gcloud in WSL
The Lens Kubernetes IDE runs on Windows but the gcloud that refreshes your GKE tokens lives in WSL. The wsl role generates .cmd shims in %USERPROFILE%\bin\ that delegate gke-gcloud-auth-plugin, gcloud, kubectl, and helm calls into WSL, and bridges the credential variables across with WSLENV. The googlecloud role's gcp-kubeconfig embeds the right account into the kubeconfig's exec.env so Lens authenticates as you, automatically.
$ gcp-kubeconfig my-cluster # writes ~/.kube/configs/my-cluster.yaml, Lens picks it upThe full walkthrough is in the guide Connect Lens on Windows to GKE through WSL.
One hotkey per client: Windows workspaces
The windows role manages the Windows half of a WSL machine, and workspaces are its everyday win: one global hotkey starts a preconfigured set of apps, each placed on its own virtual desktop and monitor, including fully isolated per-client Slack instances (--user-data-dir gives each its own sign-in):
windows_workspaces:
- name: client-a
hotkey: CTRL+ALT+2
desktop: ClientA
apps:
- command: '%LOCALAPPDATA%\slack\slack.exe'
args: '--user-data-dir=%APPDATA%\SlackProfiles\client-a'
monitor: 2
maximize: trueCtrl+Alt+2 brings up client A's Slack maximized on its own wallpapered desktop and lands you there. The full story, from the undocumented virtual desktop API to the shortcut-hotkey trick, is in the blog: One hotkey per client.
Security
Type your SSH passphrase once
ssh's Keychain integration prompts for your key passphrase a single time and keeps the agent alive across every shell session, even new terminals. Repo clones and ssh connections stop asking, and a reboot is the only thing that brings the prompt back.
Keep secrets in Bitwarden, not in your repo
No password or token has to live in your dotfiles. Unlock your vault once with bitwarden-unlock (it runs export BW_SESSION=$(bw unlock --raw) so the session is available to the whole shell), then run a playbook that reads secrets straight from the vault as it applies. The values land on disk with the right permissions; the repo only ever refers to them by name.
$ bitwarden-unlock # unlock once, session exported to the shell
$ dotpro apply # tasks read their secrets from the vault as they runA strong password on demand
bitwarden's password prints a strong 32-character password straight to your terminal, no website round-trip and nothing to copy out of a browser extension.
$ password
Qz7$mK2pLv9!nRxW3eThYbF8cJdAgU6s