Your dotfiles belong in git.
Your secrets don't.

dotf splits them. Templates go in git. Secret values stay in your password manager. On any machine, dotf sync fetches and injects them.

the problem

~/.gitconfig can't commit this
[user] name = Chris Fentiman email = chris@example.com [github] token = ghp_A1B2C3D4E5F6G7H8 # real values — keep repo private or strip them # new machine = two hours hunting what goes where

with dotf

~/.dotf/configs/.gitconfig.tmpl safe to commit
[user] name = Chris Fentiman email = {{GIT_EMAIL}} [github] token = {{GITHUB_TOKEN}}
~/.dotf/.secrets.toml safe to commit
[secrets] GIT_EMAIL = "op://personal/github/email" GITHUB_TOKEN = "op://personal/github/token"

The problem

Every dotfiles repo eventually hits this wall.

You built the perfect shell setup. You want to share it, use it on every machine, clone it in 30 seconds on a new laptop. So you push it to GitHub.

Then you grep your configs and find your email in .gitconfig, an API token in .npmrc, a registry URL with credentials in .cargo/config.toml, an internal hostname in .ssh/config. The repo has to stay private, or the secrets have to come out.

The folk solution is a .localrc or .bash_profile_priv — a file you load but never commit. It works, barely. But every new machine means manually hunting down what goes in it. The one-command setup is now a two-hour session with your old laptop open next to the new one.

73.6% of public dotfiles repos leak sensitive information. 9,452 private SSH keys. 11,758 repos with API keys. GitHub's secret scanning catches some of it — after it's already in your commit history, where deleting it doesn't help.

The leaks aren't from carelessness. They're from dotfiles that were private, then someone made public. Or configs copied between machines where one value slipped through.
~/.zshrc the classic workaround
# source secrets from a file we never commit [ -f ~/.localrc ] && source ~/.localrc # ~/.localrc on the old machine: export GITHUB_TOKEN=ghp_A1B2C3D4E5F6G7H8 export NPM_TOKEN=npm_xyz... export AWS_ACCESS_KEY_ID=AKIA...

Problem: .localrc lives only on one machine. New machine = start from scratch. Nothing documents what secrets are needed or where they come from.

new machine, day one the broken promise
# you clone your dotfiles $ git clone git@github.com:you/dotfiles.git # you run your install script $ ./install.sh # and then spend two hours figuring out # which secrets need to go in .localrc # and what their values actually were

How dotf fixes this

Three commands cover the full lifecycle.

dotf knows which secrets each config needs, where to get them, and how to put everything in place. On any machine.

01 — once per file

dotf config ~/.gitconfig

Extract

Shows you the file. You identify the secret values. dotf replaces them with {{PLACEHOLDERS}}, writes the template to your dotfiles repo, and records where each secret lives in your password manager.

02 — everyday

dotf sync

Sync

Fetches each secret from your password manager. Renders all templates. Writes the real files — gitignored, never committed. Symlinks them into place. Commits and pushes the templates.

03 — new machine

dotf init

Bootstrap

Clones your dotfiles repo. Reads .secrets.toml to check which password manager CLIs are needed. Renders everything. Symlinks everything. Done — no manual hunting.

Install

Homebrew. Two commands.

macOS and Linux via Homebrew. Windows and other platforms via cargo install dotf. Requires whichever password manager CLI you use — install it before running dotf init.

$ brew tap chrisfentiman/dotf
$ brew install dotf

Secret backends

Works with whatever you already use.

dotf routes each secret by URI scheme. Mix backends in the same .secrets.toml — personal secrets in 1Password, work secrets in Bitwarden, CI values from environment variables.

Proton Pass

pass item get <uri> --fields password

pass://vault/item/field

1Password

op read <uri>

op://vault/item/field

Bitwarden

bw get <field> <item> — requires BW_SESSION

bw://item-name/field

Environment variable

reads from process environment — useful in CI

env://VAR_NAME

Commands

Click to see the output.

zsh — ~/

project-local mode

Same mechanism. Project scope.

dotf auto-detects scope by walking up from your current directory looking for a .dotf/ directory (like git finds .git/). Templates and URI mappings commit to your project repo. Rendered files with real values stay out.

Every project has config files with secrets: .env with database credentials, .claude/settings.json with API keys, docker-compose.override.yml with registry tokens.

The usual fix? "Ask Sarah for the env file" in Slack. No schema, no validation, no way to know if your copy is stale. New contributors waste time chasing down values that could be fetched from a password manager in seconds.

dotf local mode commits the shape of your config (the template + URI mappings) so every contributor can render their own copy from their own password manager.

Security boundary: In local mode, symlink targets must resolve within the project root. Absolute paths and ~ paths are rejected. A project's .symlinks.toml cannot write outside its own directory.
project workflow local mode
# set up local mode in your project $ dotf init . # add .env with secrets extracted $ dotf config .env # render templates (no git operations) $ dotf sync # result: myproject/ .dotf/configs/ .env.tmpl ← committed .env ← rendered, gitignored .secrets.toml ← URIs only .env.dotf/configs/.env

vs. the alternatives

Other tools exist. Here's the honest difference.

Every dotfiles tool makes trade-offs. The question is which trade-offs matter for your setup.

Tool Templates Secrets Symlinks Project-local Runtime Learning curve
dotf {{VAR}} syntax Declarative .secrets.toml .symlinks.toml Auto-detect .dotf/ Rust binary Low
chezmoi Go text/template Embedded in templates File copies -- Go binary Medium-high
GNU Stow -- -- Symlink farm -- Perl Very low
yadm Jinja2 (limited) Git-crypt (whole file) -- -- Bash script Low
dotbot -- -- YAML config -- Python Low
home-manager Nix expressions agenix/sops-nix Nix-managed -- Nix Very high
rcm -- -- Tag-based -- Bash Low

chezmoi closest competitor

chezmoi is the most feature-complete dotfiles manager available. It has secret integration, run-once scripts, external file fetching, and OS-conditional logic. If you need all of that, use chezmoi.

The trade-off is complexity. chezmoi introduces its own concepts: source state vs. target state, filename-prefix attributes (dot_, private_, run_once_, modify_), and a Go template dialect with Sprig functions. Secrets are embedded directly in templates: {{ (bitwarden "item").password }}. This couples your templates to a specific password manager — switching from Bitwarden to 1Password means editing every template that references a secret.

dotf separates secrets from templates. Your template says {{DB_PASS}}, and .secrets.toml maps it to bw://db/password. Switch password managers by changing one line, not every template. Your dotfiles directory stays a plain git repo — no source-state directory, no filename prefixes, no migration step.

Go binary 16k+ GitHub stars file copies, not symlinks Go text/template syntax

GNU Stow great for simple setups

Stow is pure symlink management. It's elegant, transparent, and has zero learning curve for the core concept: mirror a directory tree as symlinks. If your configs have no secret values — no tokens, no emails, no credentials — use Stow. It's simpler than dotf.

If they do have secrets, Stow has nothing to say. You're back to the .localrc workaround or manually managing secrets outside of git. Stow also has no templating, no diffing, no sync workflow, and no way to handle machine-specific configs. dotf is Stow with a template-render-and-inject step and git sync built in.

Perl no templates no secrets Unix only

yadm git-native approach

yadm is a thin wrapper around a bare git repo. If you think in git, yadm feels natural. Its secret handling is GPG encryption via git-crypt — whole files, all or nothing. You can't encrypt just the email field in a .gitconfig; the entire file becomes an opaque blob. git diff and git log show nothing useful for encrypted files.

Machine-specific configs use "alternate files" — full copies of a file per hostname or OS. This doesn't scale: 5 machines with slightly different .zshrc files means 5 copies to maintain, not 1 template with a variable. dotf doesn't encrypt anything. Your password manager already handles storage and access control. dotf just asks it at sync time, so the readable template stays in git and the real file stays out.

Bash script bare git repo GPG whole-file encryption alternate files, not templates

dotbot simple and YAML-driven

dotbot is a YAML-driven symlink + shell command runner. Clean config format, plugin ecosystem, cross-platform. Like Stow, it has no templating, no encryption, no secrets, and no diff/preview. If you just need to declare "these files should be symlinked here" and occasionally run a shell script, dotbot does that well. If any of those files contain values that shouldn't be in git, dotbot can't help.

Python runtime YAML config no templates no secrets

home-manager different paradigm

home-manager manages dotfiles, packages, services, and system configuration through Nix expressions. It's the most powerful option — fully declarative, reproducible, with atomic rollbacks. It also requires learning the Nix language, which has a notoriously steep curve and famously unhelpful error messages. If you're already invested in the Nix ecosystem, home-manager is excellent. If you just want to manage some config files with secrets, it's like using a bulldozer to plant flowers.

Nix ecosystem steep learning curve no Windows manages everything, not just dotfiles

git-crypt / git-secret encryption approach

These encrypt specific files in your repo. The encrypted blob is opaque — git diff shows binary noise, git log shows nothing meaningful. Key management is its own problem: rotate your GPG key and every collaborator's access breaks until you re-encrypt.

The decrypted value still has to go somewhere on the new machine — you've moved the secret-distribution problem, not solved it. dotf routes to your password manager, which you already have on every machine, handles its own access control, and doesn't require GPG key ceremony.

GPG-based whole-file encryption key management overhead opaque diffs

rcm tag-based organization

rcm from Thoughtbot uses a tag-based system to group related dotfiles and supports multiple dotfile directories. Simple shell commands (rcup, lsrc, mkrc). No templates, no secrets, no encryption. Unix only with minimal development activity. Good if you want organized symlinks across personal and work configs, but doesn't address the secret values problem.

Bash tag-based groups no templates no secrets Unix only