dotfiles
Places the raw dotfiles you list in dotfiles_list into $HOME on every apply: links, copies, or templates them, backs up the files it adopts, and prunes links you drop from the list. It folds the job stow or chezmoi apply would do into the run that already provisions the machine.
Demo

What it does
- Links, copies, or templates each entry in
dotfiles_listto its path under$HOME - Creates the missing parent directories, so
.config/starship.tomllands even on a fresh machine, and leaves the permissions of the ones that already exist alone - Backs up a pre-existing real file to
<path>.dotpro.bakthe first time it adopts the path - Records managed paths and prunes the symlinks for entries you remove
Variables
| Variable | Default | Description |
|---|---|---|
dotfiles_list | [] | Dotfiles the role places in $HOME (see below) |
dotfiles_dir | | Base directory the dotfiles_list sources are resolved against |
dotfiles_mode | link | Default placement for entries that do not set their own: link, copy, or template |
dotfiles_backup | true | Move a pre-existing real file to <path>.dotpro.bak the first time the role adopts its path |
dotfiles_ignore | [] | Glob patterns (matched on source path and basename) dropped from glob and directory entries |
dotfiles_state | ~/.local/state/dotpro/dotfiles | Manifest of managed paths, used to prune entries you remove |
Managing dotfiles
Not every file wants to be a template. A hand-tuned starship.toml or a long .bashrc is happiest committed verbatim and dropped into $HOME as-is. List those files in dotfiles_list and the role puts them in place on every apply, the job stow or chezmoi apply would do, but folded into the run that already provisions the machine:
dotfiles_list: # default: []
- .bashrc
- .gitconfig
- .config/starship.tomlEach entry is a path relative to dotfiles_dir (the repo root by default) and is linked to the same path under $HOME. Parent directories are created as needed, so .config/starship.toml links into ~/.config/ even on a fresh machine.
Where each file lands
A bare string links to the matching path under $HOME. Use a mapping to send a file somewhere else, or to copy or template it instead of linking:
dotfiles_list:
- .bashrc # ~/.bashrc -> repo/.bashrc
- src: shell/aliases.sh # ~/.config/shell/aliases.sh -> repo/shell/aliases.sh
dest: .config/shell/aliases.sh
- src: gitconfig.j2 # rendered through Jinja, not linked
dest: .gitconfig
mode: template # default: dotfiles_mode (link)
- src: ssh_config # some apps reject a symlinked config; copy it
dest: .ssh/config
mode: copylink points back at the repo, so editing the source and re-applying needs no re-render. copy and template write a real file (template runs it through Jinja with the play's variables first), for the handful of apps that refuse to follow a symlinked config.
Globs and whole directories
A src containing a glob character (*, ?, [) expands per file; a plain directory path is placed whole as a single link. dotfiles_ignore drops matches by path or basename:
dotfiles_ignore: # default: []
- "*.md"
dotfiles_list:
- .config/nvim # the whole directory, one symlink
- shell/*.zsh # each match mirrored to ~/shell/<name>.zsh
- src: bin/* # reparented: each match -> ~/.local/bin/<name>
dest: .local/binSources outside the repo
An absolute src is taken as-is instead of being resolved against dotfiles_dir, so a file kept in another checkout (a handbook, a notes repo) can be placed as well. It needs an explicit dest, because an absolute path carries no sensible relative destination:
dotfiles_list:
- src: /projects/handbook/notes # ~/notes -> /projects/handbook/notes
dest: notesSuch a link is only as good as the checkout behind it: on a machine that lacks the path the apply still places the symlink and it dangles. Pruning leaves it alone too, since the role only removes stale symlinks pointing back into dotfiles_dir.
Adoption and pruning
The role manages a set, not just a list, so it reconciles $HOME to what you declare.
The first time it takes over a path that already holds a real file (the distro's default .bashrc, say), it moves that file to <path>.dotpro.bak before placing its own, unless dotfiles_backup is false. Paths it already manages are updated in place, so the backup happens once, on adoption.
Adopting a whole directory is the one thing dotpro plan cannot show you. Moving it aside is a command, which check mode skips, so the plan reports the adoption in words and leaves the link to the apply.
It records every managed path in dotfiles_state. Drop an entry from the list and the next apply removes the symlink it left behind. A dropped copy or template target is a real file, and a dropped link to a source outside dotfiles_dir is not the role's to reclaim, so both are reported for you to delete by hand rather than removed automatically.
Files you would rather drive from data (a .gitconfig keyed on per-machine identity, for one) belong in a role instead; the git role is built for exactly that. For secrets, reach for Ansible Vault and the bitwarden role rather than committing them as plain dotfiles, and for per-host variants set dotfiles_list differently in host_vars or behind a when.
Combining with settings roles
Most config in this collection is driven by data, not files: the git role builds ~/.gitconfig from git_config, starship renders ~/.config/starship.toml from starship_config, and claude merges claude_code_settings into ~/.claude/settings.json. That is the "settings, not files" path, and it is the one to reach for first: you get merged defaults, cross-platform behavior, and per-host overrides from a single declaration.
This role is the other path, a file committed verbatim and linked into place. The two are complementary and are built to coexist on the same path.
Write-through is the default
The collection's config roles are built to share a path with this one. They write through a symlinked target instead of replacing it, so linking a file with the dotfiles role and also driving it from a role's vars lands the role's output in the linked repo file, ready to commit:
git config(thegitrole) resolves the symlink natively and writes to the link target.- The
templateandcopywriters (starship,claude,kubernetes) setfollow: true, so they too edit through the link. - The roles that merge (
claude,kubernetes) read the same path back first, so your hand edits in the repo file are preserved and merged, not overwritten.
Order still matters: place the dotfiles role before the settings roles in the play so the link already exists when they write. If a settings role runs first on a fresh machine and the file is not there yet, it creates a regular file, and the later dotfiles step links over it.
Even with write-through, a path still has one logical source of truth. Two clean choices and one hybrid:
- "Settings, not files" drives the values through the role's vars and leaves the file out of
dotfiles_list; the rendered file is disposable output. - A verbatim file lists itself in
dotfiles_listwith no role vars set for it; the committed file is the source of truth. - Write-through capture, the hybrid, links the file and drives it, accepting mixed ownership for the benefit that edits made outside Ansible are captured into the repo.
For files no role manages (a hand-tuned .bashrc, .config/nvim, AGENTS.md), there is no question to answer, and this role is simply the tool.
Commands
The dotpro role ships dotpro dotfiles, which lists each managed dotfile and its link state (ok / broken / file / missing); dotpro dotfiles status previews drift with a --check apply of the dotfiles tag.