One command. Every dotfile you care about, collected into a timestamped folder, zip or tar.gz.
Your .env files come along; your private keys never do. No config file required.
Nothing to install and no config file to write. In a terminal it walks you through what it found;
with --auto it just runs.
npx create-dotfiles # interactive
npx create-dotfiles --auto # no prompts, defaultsRather have it on your PATH? Install it with Homebrew or npm and run
create-dotfiles instead of npx create-dotfiles.
Not sure what it would take from your home directory? Ask first. This writes nothing:
$ npx create-dotfiles --auto --dry-run
Would copy 8 files (225 B) from /Users/you
.zshrc (37 B) [core]
.gitconfig (46 B) [core]
.config/nvim/init.lua (10 B) [core]
.config/nvim/lua/plugins.lua (11 B) [core]
.tmux.conf (16 B) [core]
.ssh/config (27 B) [core]
.npmrc (40 B) [secrets]
work/api/.env (38 B) [secrets]
Per group: core 6, custom 0, secrets 2, config-all 0
Not found (59): .zshenv, .zprofile, .bashrc, ...
Would write:
folder: /Users/you/dotfiles-20260904-083845/
Dry run: nothing was written.Drop --dry-run and that plan becomes ~/dotfiles-20260904-083845/, every file at its path
relative to your home directory. ~/.config/nvim was a symlink on this machine and came through as
real files, so the collection stands on its own. Add --format folder,zip,tar for archives of the
same tree beside it.
Secrets come along by default. .env files found by a bounded scan, plus .npmrc, .yarnrc,
.netrc, .aws/credentials and .docker/config.json. Pass --no-include-env to leave them out.
SSH and GPG private keys are never copied either way.
On the other machine, put it back:
$ npx create-dotfiles restore
Restoring /Users/you/dotfiles-20260904-083845 into /Users/you
[OK] .config/nvim/init.lua
[OK] .config/nvim/lua/plugins.lua
[OK] .gitconfig
...
Restored 8 files, 0 skipped, 0 failed.restore takes the newest collection folder and never overwrites a file that is already there; run
it twice and the second run reports [SKIP] .zshrc exists (use --force). Extract a zip or tar.gz
first, since restore reads a folder.
Next: See it in action for the interactive flow, Flags for every option, and What gets collected for the full target list and the never-copied rules.
Three ways to run it. They are the same program; pick by how you like your tools delivered.
| Command afterwards | Updates | Needs | |
|---|---|---|---|
| Homebrew (macOS, Linux) | create-dotfiles |
brew upgrade create-dotfiles |
Homebrew (it brings its own Node) |
| npm | create-dotfiles |
npm i -g create-dotfiles again |
Node 22 or newer |
| npx, no install | npx create-dotfiles |
automatic (@latest to be sure) |
Node 22 or newer |
Tap and trust once per machine, then install by name:
brew tap thilllon/tap
brew trust thilllon/tap
brew install create-dotfilesHomebrew 6 and later load formulae only from taps you trust, which is what brew trust is for; a
tap that is not trusted refuses brew install create-dotfiles. brew install thilllon/tap/create-dotfiles does all three steps in one command, trusting just this formula. The
formula lives in thilllon/homebrew-tap. It installs
Homebrew's node as a dependency and builds in a few seconds, with the developer tools Homebrew
already relies on.
Then use the create-dotfiles command. Everything in this README written as
npx create-dotfiles … works the same as create-dotfiles …:
create-dotfiles # interactive
create-dotfiles --auto # no prompts, defaults
create-dotfiles --auto --dry-run # show the plan, write nothing
create-dotfiles --format zip --encrypt-zip # password-protected zip
create-dotfiles restore # put the newest collection back
create-dotfiles --help # every flag and rule
create-dotfiles --versionNew releases reach the tap within a few hours of npm. brew upgrade checks for them once a day
on its own; to get one right away:
brew update && brew upgrade create-dotfilesTo remove it (the last two lines are only needed if you want the tap gone too):
brew uninstall create-dotfiles
brew untap thilllon/tap
brew untrust thilllon/tapIn a Brewfile, trusted: true does the brew trust step for you:
tap "thilllon/tap", trusted: true
brew "create-dotfiles"npm i -g create-dotfiles
create-dotfiles --versionNode 22 or newer. Run the install again to update. pnpm (pnpm add -g create-dotfiles, after
pnpm setup) works too, but pnpm 11 installs a release only once it is a day old. So does Yarn 1
(yarn global add create-dotfiles); Yarn 2 and later have no global installs.
npx create-dotfiles runs it without installing anything but Node 22 or newer, and picks up a
newer release on its own. If create-dotfiles is also installed (with Homebrew, npm i -g, or in
the current project), npx runs that copy instead, even an older one; npx create-dotfiles@latest
always gets the newest release. Quickstart and the transcripts in this README use this form; with
Homebrew or npm, drop the npx.
Run it in a terminal and it walks you through what it found, what it will never touch, and what you want back:
- Scans your home directory once and shows which known dotfiles exist on this machine, grouped (Shell, Git, Editors, Tools, …).
- Lists what is never copied:
node_modules,.git, caches, SSH/GPG private keys, files over 10 MB. - Asks whether to include secret files —
.envfiles found by the scan (it tells you how many), plus.npmrc,.netrc,.aws/credentials,.docker/config.json. Default: yes. - Asks whether to include everything under
~/.config(with a file count and size). Default: no. - Lets you pick output formats —
folder(preselected),zip,tar.gz— any combination. - Shows the exact output paths and asks to proceed. Cancelling at any step writes nothing.
Scripted, it just does the sensible thing. This is a real run against a home directory with a
symlinked ~/.config/nvim, a .env two levels deep, another .env inside node_modules, an SSH
key pair and a 20 MB file:
$ npx create-dotfiles --auto --format folder,zip,tar
Copied 9 files (132 B) from /Users/you
.zshrc (13 B) [core]
.gitconfig (20 B) [core]
.config/nvim/init.lua (8 B) [core]
.config/nvim/lua/plugins.lua (11 B) [core]
.tmux.conf (16 B) [core]
.hammerspoon/init.lua (15 B) [core]
.ssh/config (13 B) [core]
.npmrc (27 B) [secrets]
projects/app/.env (9 B) [secrets]
Per group: core 7, custom 0, secrets 2, config-all 0
Skipped, larger than 10 MB (1):
.hammerspoon/Spoons/blob.bin (20.0 MB)
Not found (44): .zshenv, .zprofile, .bashrc, ...
Written:
folder: /Users/you/dotfiles-20260902-150719/
zip: /Users/you/dotfiles-20260902-150719.zip
tar.gz: /Users/you/dotfiles-20260902-150719.tar.gzThe symlinked Neovim config came through as real files, node_modules/pkg/.env and the private key
did not, and the oversized file was reported rather than silently dropped. Every file keeps its
home-relative path, so the folder is a faithful mirror and the archives restore anywhere.
| ▸ Zero setup | No config file needed. Built-in targets cover shells, Git, Vim/Neovim, tmux, terminal emulators, Starship, fish, mise, VS Code and Cursor, and more — with the right paths on macOS, Linux and Windows. |
| ▸ Interactive or scripted | A terminal gets prompts; --auto gets defaults. Piped or in CI, it falls back to --auto on its own. |
| ▸ Timestamped, faithful | ~/dotfiles-YYYYMMDD-HHMMSS/ mirrors home-relative paths. Get a folder, a zip, a tar.gz, or all three in one run. |
| ▸ Secrets in, keys out | .env files (found by a bounded scan), .npmrc, .netrc, .aws/credentials, .docker/config.json are included by default; --no-include-env leaves them out. SSH and GPG private keys are never copied, ever. |
| ▸ Encrypted zip | --encrypt-zip protects the zip with WinZip AES-256 (AE-2). The password comes from a hidden prompt, or is generated for you (150 bits); never from the command line. |
| ▸ Never copies junk | node_modules, .git, caches, previous collections and files over 10 MB are skipped — and the skips are reported, not hidden. |
| ▸ Symlinks resolved | A stow-style symlinked ~/.config/nvim is copied as real files, at every level, so nothing in the archive points back at a machine you no longer have. |
| ▸ Safe restore | restore puts the newest collection back without overwriting anything unless you pass --force. |
| ▸ Dry run | --dry-run prints exactly what would be copied, with sizes and groups, and writes nothing. |
| ▸ Typed library | import { collect, restore } from "create-dotfiles" ships with full TypeScript declarations and zero runtime dependencies. |
The examples below use the installed command (Install). Without installing, put npx
in front: npx create-dotfiles --auto.
create-dotfiles # interactive
create-dotfiles --auto # defaults: core targets + secrets, folder output
create-dotfiles --dry-run # show the plan, write nothing
create-dotfiles restore # put the newest collection back (never overwrites)Every flag works with --auto and, in interactive mode, pre-fills the corresponding question.
| Flag | Default | Effect |
|---|---|---|
--auto |
off | Run without prompts. |
--include-env |
on | Include the secrets group (.env files and the credential files listed above). --no-include-env turns it off. |
--include-config |
off | Include everything under ~/.config, minus the never-copied rules. |
--format <list> |
folder |
Comma-separated subset of folder, zip, tar (tar.gz and tgz are accepted). |
--out <dir> |
~ |
Parent directory for the output. ~/ is expanded. |
--max-file-size <mb> |
10 |
Skip files larger than this; they are listed in the summary. |
--dry-run |
off | Print the plan and the output paths without writing anything. |
--encrypt-zip |
off | Encrypt the zip with AES-256; see Password-protected zip. --no-encrypt-zip overrides the config. |
--help and --version never read or write anything in your home directory.
create-dotfiles --format zip --encrypt-zip # asks for the password (hidden), twice
CREATE_DOTFILES_ZIP_PASSWORD_FILE=~/.dotfiles-zip-password \
create-dotfiles --auto --format zip --encrypt-zip- Encryption. WinZip AES-256 in its AE-2 form, the strongest zip encryption that common tools open. Every file is encrypted with AES-256 and authenticated with HMAC-SHA1, and no plaintext CRC is stored. The old ZipCrypto scheme, which a known-plaintext attack breaks, is never used.
- The password is the real protection. The zip format fixes its key derivation at
PBKDF2-HMAC-SHA1 with 1000 iterations, so one GPU tries tens of millions of guesses a second.
- A typed password must be at least 15 characters (the NIST SP 800-63B minimum). It can use only printable ASCII and at most 99 characters, the limits 7-Zip accepts.
- Leave the prompt empty to generate one: 30 random characters (150 bits), such as
7K2QM-4XH9T-…. It is shown once, and it is used only after you confirm you have stored it.
- Where the password comes from. In a terminal, the hidden prompt, always. With
--auto(or with no terminal),CREATE_DOTFILES_ZIP_PASSWORD, or the first line of the file named byCREATE_DOTFILES_ZIP_PASSWORD_FILE. No flag takes the password, because the command line shows up inpsand in your shell history. The config file never holds it either. Without a usable password the run stops before writing anything; it never falls back to a plain zip. - What stays visible. File names, sizes and dates, as in any zip. A folder or tar.gz written in the same run is not encrypted, and the summary says so. Without the folder among the formats, the files are staged in a private temporary folder rather than next to the zip, so no plaintext copy ever reaches the output directory (a synced folder or a USB stick, say).
- Opening it.
bsdtar -xf dotfiles-….zipasks for the password.bsdtaris built into macOS (wheretaris the same program); on Linux it comes with libarchive-tools. 7-Zip, Keka and The Unarchiver open it too.unzip,dittoand GNU tar cannot open AES zips.
create-dotfiles restore # newest ~/dotfiles-YYYYMMDD-HHMMSS
create-dotfiles restore ~/dotfiles-20260902-150719
create-dotfiles restore --force # overwrite files that already existFiles that already exist are reported as [SKIP] <path> exists (use --force). Restore works from a collection folder; extract a zip or tar.gz first (an encrypted zip with bsdtar -xf, which asks for the password).
The built-in list is the common set plus the set for the OS you run on; targets from the other
platforms are never attempted, so they do not show up under "Not found". Every path is relative
to your home directory (%USERPROFILE% on Windows) and written with / in the summary and the
archives on every OS. Only the entries that exist on your machine are copied.
Common (every OS)
- Shell:
.zshrc.zshenv.zprofile.bashrc.bash_profile.profile.inputrc - Git:
.gitconfig.gitignore_global.gitattributes_global - Editors:
.vimrc.ideavimrc.config/nvim.editorconfig - Terminal:
.tmux.conf.config/tmux.config/starship.toml.config/alacritty.config/kitty.config/wezterm.wezterm.lua.config/ghostty.config/fish.config/zellij - Tools:
.config/mise.tool-versions.config/gh/config.yml.config/htop.config/bat.config/lazygit - Non-secret parts of secret-adjacent tools:
.ssh/config.gnupg/gpg.conf.gnupg/gpg-agent.conf.aws/config
macOS
- VS Code and Cursor:
Library/Application Support/{Code,Cursor}/User/{settings.json,keybindings.json,snippets} .hammerspoon.config/karabiner.skhdrc.yabairc.BrewfileBrewfile
Linux
- VS Code, Code - OSS, VSCodium and Cursor:
.config/{Code,Code - OSS,VSCodium,Cursor}/User/{settings.json,keybindings.json,snippets} .bash_logout.xinitrc.xprofile.Xresources.config/i3.config/sway.config/hypr.config/waybar.config/rofi.config/dunst.config/picom.config/polybar.config/gtk-3.0/settings.ini.config/fontconfig
Windows (relative to %USERPROFILE%)
- VS Code and Cursor:
AppData/Roaming/{Code,Cursor}/User/{settings.json,keybindings.json,snippets} - Neovim:
AppData/Local/nvim(in addition to.config/nvim) - Windows Terminal:
AppData/Local/Packages/Microsoft.WindowsTerminal_8wekyb3d8bbwe/LocalState/settings.json - PowerShell:
Documents/PowerShell/Microsoft.PowerShell_profile.ps1Documents/WindowsPowerShell/Microsoft.PowerShell_profile.ps1 AppData/Roaming/alacritty.wslconfig
Secrets (on by default; --no-include-env to skip) — .npmrc .yarnrc .netrc .aws/credentials .docker/config.json, and every .env / .env.* found by a scan of your home directory that goes at most four levels deep, never enters the never-copied directories, does not follow symlinked directories (or junctions), and skips the top-level user folders: Library, Desktop, Documents, Downloads, Movies, Music, Pictures, Public (macOS, so it never asks for folder access), Videos, Templates, snap (Linux) and AppData, Application Data, Local Settings, OneDrive, Contacts, Favorites, Links, Saved Games, Searches, 3D Objects (Windows). Core targets inside those folders, such as VS Code settings under ~/Library or AppData, are unaffected. An entry the scan cannot read (a locked file, a junction that refuses access) is reported under "Failed" and skipped; the run continues.
Everything under ~/.config (--include-config) — after the never-copied rules and the size cap.
Never copied, regardless of options
- Directories named
node_modules.git.hg.svn__pycache__.venvvenv.cacheCacheCachesCachedDataCode CacheGPUCacheService Worker.npm.pnpm-store.yarn.cargo.rustup.gradle.m2.TrashTrash, plus.DS_Store Library/Cachesand anyLibrary/Application Support/*/Cache*- Private keys: everything in
.ssh/exceptconfigand*.pub;.gnupg/private-keys-v1.d,.gnupg/*.gpg,.gnupg/*.kbx - Previous collections (
dotfiles-YYYYMMDD-HHMMSS,.zip,.tar.gz), so a secrets scan never re-collects an earlier run - Files larger than the size cap (default 10 MB)
If ~/.dotfilesrc.toml exists it is read; it is never created for you.
[settings]
include_env = true
include_config = false
encrypt_zip = false # the password is never read from this file
formats = ["folder", "zip"]
max_file_size_mb = 10
out = "~/Backups"
[files]
# Extra paths, relative to your home directory.
include = [".config/foo", "work/scripts"]
# Paths or directory names to add to the excludes.
exclude = [".config/kitty", "Snapshots"]Explicit flags win over the file, which wins over the built-in defaults. Entries must be relative and stay inside your home directory; anything else is rejected with a clear error. On Windows you can write them with backslashes (AppData\Roaming\Code); they are normalised to /. An include that hits a never-copied rule is reported under "Failed" rather than silently dropped.
import { collect, restore } from "create-dotfiles";
const summary = await collect({ formats: ["folder", "zip"], includeEnv: false });
console.log(summary);
// AES-256 zip; a function is called only if the zip really is encrypted.
await collect({ formats: ["zip"], encryptZip: true, zipPassword: () => readSecretSomehow() });
restore({ force: false });collect, resolveTargets (the planning step behind --dry-run), restore, runInteractive (bring your own prompter), the option and summary types, DEFAULT_TARGETS (this OS) and targetsFor(platform) are all exported with declarations. Pass platform: "win32" (or "darwin", "linux") in the options to plan for another OS. Everything is bundled; the package has no runtime dependencies.
- Nothing is written until the plan is final. Interactive cancels and
--dry-runleave your disk untouched. - Output never merges. If
dotfiles-<timestamp>already exists, the run stops withOutput already existsinstead of writing into it. - Copies stay inside the collection. Every path is validated to be relative and confined to your home directory before anything is copied.
- Encryption never degrades silently.
--encrypt-zipwithout a usable password stops before anything is written; it never falls back to an unencrypted zip. - Failures are per file. One unreadable file is reported and the rest proceeds; the summary lists every skip and failure.
mise install # node, pnpm and lefthook, pinned in mise.toml
pnpm install # also installs the git hooks
pnpm dev # run src/cli.ts with tsx| Script | What it does |
|---|---|
pnpm test |
Vitest |
pnpm test:coverage |
Vitest with the coverage thresholds enforced |
pnpm lint |
Biome check |
pnpm format |
Biome check --write |
pnpm typecheck |
tsc --noEmit |
pnpm build |
tsdown → dist/cli.cjs, dist/index.cjs, dist/index.d.cts, plus the lazily loaded zip.js chunk |
mise run ci |
The whole CI job locally, with CI=true |
Releases are automated: dependabot's minor and patch updates are merged and published as soon as CI is green, and releases run only from GitHub Actions with npm Trusted Publishing. The Homebrew tap picks up each npm release by itself after checking its provenance. See AGENTS.md for the details.
MIT