# Dotter, a dotfile templater whose --force cancels --dry-run

> Dotter deploys a git dotfile repository as symlinks and handlebars templates, driven by two TOML files, four hook scripts, a cache and a watch loop. Its own help text lets --force override --dry-run, which is the detail to understand before the first real deploy.

**SuperCuber/dotter** — A dotfile manager and templater written in rust 🦀

- Repository: https://github.com/SuperCuber/dotter
- Stars: 2,022 · Forks: 78
- Language: Rust
- License: Unlicense
- Published: 2026-10-14 · Updated: 2026-10-14 · Language: en
- Canonical page: https://hysenlabs.com/projects/supercuber-dotter

## Three failures of a hand-linked dotfile repository

Most people already keep their configuration in a git repository and point their home directory at it with `ln -s`. Dotter names three things that arrangement cannot do once the dotfile collection passes a handful: you stop knowing which file came from where, a new machine means creating every single link by hand, and nothing in the scheme can express a difference between two machines. The worked example given for that last problem is a battery meter that belongs on a laptop status bar and not on a desktop. All three are configuration problems rather than file management problems, and that split is what Dotter works on. It keeps the repository and the symlink, then inserts a configuration layer that decides what goes where on each machine, plus a templating pass so one source file can expand into more than one target.

## A .dotter directory holds config, cache and four hook scripts

Every default path is relative to the working directory, so the repository root is where dotter has to be started from. Six flags move those locations: global configuration in `.dotter/global.toml`, per-machine selection in `.dotter/local.toml`, the record of what went live in `.dotter/cache.toml`, the stored payloads in `.dotter/cache`, and four optional shell hooks named `.dotter/pre_deploy.sh`, `.dotter/post_deploy.sh`, `.dotter/pre_undeploy.sh` and `.dotter/post_undeploy.sh`.

```bash
  -g, --global-config <GLOBAL_CONFIG>
          Location of the global configuration [default: .dotter/global.toml]
      --cache-file <CACHE_FILE>
          Location of cache file [default: .dotter/cache.toml]
```

The help output calls `deploy` the default subcommand, and `init` is the bootstrap step. What `init` writes is not usable as it stands: the generated global.toml holds one package covering every file in the current directory, with each target pointing at a dummy value, and a local.toml that selects that package. Filling in real destinations is manual work, and the README does not show a configuration file with keys in it at any point.

## --force overrides --dry-run, and --patch assumes --noconfirm

The flag interactions are where the help output contradicts itself in a way worth planning around. `-d, --dry-run` promises to print information without doing anything, and `-f, --force` answers with the single line "Overrides --dry-run". A command line carrying both flags is not a rehearsal. It is a real deploy that also happens to print a diff, and nothing in the output marks that difference.

```bash
  -d, --dry-run
          Dry run - don't do anything, only print information. Implies -v at least once
  -f, --force
          Force - instead of skipping, overwrite target files if their content is unexpected. Overrides --dry-run
```

Verbosity is what produces that diff: specifying it up to 3 times gives more detail, and at least one mention prints the differences between what was before and after the run. `--diff-context-lines` sets how many lines surround each hunk, three by default. `-p, --patch` is the second coupling. It reads standard input as an extra files and variables patch applied after `local.toml` is evaluated, and it assumes `--noconfirm`, which is the flag that answers yes on your behalf when empty directories are removed. Since all of stdin becomes the patch, a CI step that also expects to pipe something else has nothing left to send.

## The watch subcommand runs forever on a binary that aborts

`watch` turns the same repository into a loop, described in the help output as watching the repository for changes and deploying as soon as they happen, and it accepts `--dry-run` to preview that loop instead of writing. Three optional crates back it, `watchexec`, `watchexec-events` and `watchexec-filterer-globset`, and the manifest line `default = ["scripting", "watch"]` means a plain build from crates.io already carries them. The same default block also pulls in `handlebars/script_helper`, so scripting inside templates is on unless you opt out.

That default interacts with `panic = "abort"` under the manifest's profile settings. A panic does not unwind a single deploy step and let the loop continue; it ends the process, and in watch mode the process is the loop itself. `opt-level = "z"`, `lto = true` and `codegen-units = 1` point the same build at a small binary rather than a fast one, which fits a tool that lives in `$PATH` and runs on every file save. What the README does not document is what a half finished deploy leaves behind when the process dies mid write.

## Homebrew, Scoop and three AUR packages install different builds

Four paths cover the platforms the repository targets. On macOS, Homebrew serves the binary with `brew install dotter`. Arch users get three AUR packages sitting at different points on the version line: `dotter-rs-bin` gives a precompiled binary of the newest published build, `dotter-rs` compiles the source of that build on the machine itself, and `dotter-rs-git` compiles whatever commit master currently carries.

```bash
brew install dotter
scoop install dotter
cargo install dotter
```

The README credits orhun with maintaining all three AUR packages. Windows users run `scoop install dotter`, and elsewhere the binary is downloaded and placed in `$PATH`, or dropped into the dotfile repository and run as `./dotter`. Because all configuration defaults are relative paths, that second option only works from the repository root. Windows is also the one platform with its own dependency, `dunce = "1.*"`, declared in a `cfg(windows)` block while unix gets `libc = "0.2.180"`; the README does not say what either crate is used for.

## cargo install dotter needs a toolchain that accepts edition 2024

The crate path is the least forgiving one, because the manifest is strict about the toolchain: edition 2024 together with rust-version 1.85. An older rustc refuses the build instead of falling back. Dependency versions are pinned in mixed ways, which is what a project shipping for years looks like. The parser is clap at 4.4.18 with derive enabled, while clap_complete sits at 4.4.10 feeding the `gen-completions` subcommand, so the completion generator tracks an earlier point in the same 4.4 line than the parser it serves.

Templating comes from `handlebars = "6.*"`, with `handlebars_misc_helpers` at `0.17.*` carrying only the string and json features. Expression evaluation sits in evalexpr at 13, path expansion in `shellexpand = "2.*"`, host naming in hostname at 0.3.x, terminal handling in crossterm at 0.29.0 and the comparison output in `diff = "0.1.*"`. None of that wiring is explained in prose, and the schema of the two configuration files appears nowhere in the README, only in the wiki. The last push was on 2026-07-13, and the manifest carries version 0.13.5.

## undeploy only knows about files still in cache

`undeploy` has the narrowest promise in the help output. It deletes deployed files from their target locations and, the help says plainly, operates on all files that are currently in cache. The record of a deploy is therefore the cache directory and its TOML file, both living inside `.dotter/` in the same repository as the configuration itself. Lose them and dotter has no list of what it placed on your system.

```bash
  undeploy         Delete all deployed files from their target locations. Note that this operates on all files that are currently in cache
  init             Initialize global.toml with a single package containing all the files in the current directory pointing to a dummy value and a local.toml that selects that package
```

The same cache governs the skip rule. `-f, --force` exists precisely because a target whose content is unexpected is skipped by default, which protects a file you edited by hand and also leaves that file permanently divergent from your repository until you overwrite it on purpose. The README does not document rollback, and no command in the help output reports what is currently deployed other than reading the cache file yourself.

## Templating runs through handlebars, with scripting on by default

The alternative the README names is the barebones `ln -s` arrangement: hand made symlinks, no configuration file, no per machine selection and no templating. Dotter keeps the symlink and the git repository and inserts a two file configuration layer, a handlebars pass and four hooks between them, and the cost of that layer is concrete, because the keys of `global.toml` and `local.toml` are not in the README. The repository topic list carries stow, placing the project alongside symlink tree managers, while the manifest's separate unix and windows dependency blocks acknowledge that creating links is the platform sensitive part.

Configuration handling runs through `toml = "1"` with `serde`, output goes through the log crate and `simplelog`, and async work uses `tokio`. There is no separate test directory at the top level, so the unit tests built on mockall at 0.12.1 sit inside `src/`, with a commented out nightly variant kept in the manifest for better failure messages. Dotter ships under the Unlicense, a public domain dedication with no conditions attached, so an inbound ownership review has nothing to trace. The mechanism itself, including every configuration key, is in the wiki rather than in the repository.

## Conclusion

Dotter suits someone already running a git dotfile repository across two machines, since the two-file config, four hooks and the cache give a real control surface where manual symlinks give none. Leave it alone if your configuration is a handful of files, or if a tool whose config schema lives only in the wiki is a problem. Before the first deploy, run dotter with `--dry-run`, confirm that no `--force` is set, and check what sits in `.dotter/cache` afterwards. If that cache directory is ever deleted, `undeploy` has no list of deployed files to work from, so keep it under version control with the rest of the repository.

## FAQ

### Does dotter work on Windows?

Yes, through Scoop: `scoop install dotter` is the documented Windows path. Cargo.toml also carries a Windows only dependency, `dunce = "1.*"`, in a `cfg(windows)` block, while unix gets `libc = "0.2.180"`.

### What does dotter init actually generate?

The help output says it initializes global.toml with a single package containing all the files in the current directory pointing to a dummy value, plus a local.toml that selects that package. The dummy values have to be replaced with real target paths before any deploy does anything useful.

### Can dotter roll back a deploy?

No rollback command appears in the help output. The nearest option is `undeploy`, which deletes all deployed files from their target locations and operates on all files that are currently in cache, so it depends entirely on that cache surviving.

### Where does dotter look for its configuration?

The defaults are `.dotter/global.toml`, `.dotter/local.toml`, `.dotter/cache.toml` and the `.dotter/cache` directory, all relative paths that the global-config, local-config and cache-file options can move elsewhere. Four more flags point at the optional hook scripts.

### How do I preview what a deploy would change?

Pass `--dry-run` to print information without doing anything. At least one verbosity flag prints the differences between what was before and after the run, and `--diff-context-lines` controls the surrounding context, three lines by default.

### What Rust toolchain does dotter need?

Cargo.toml sets edition 2024 and `rust-version = "1.85"`, so `cargo install dotter` needs a rustc at or above that version. An older toolchain refuses the build rather than building an older edition.

## Sources

- [Issues](https://github.com/SuperCuber/dotter/issues)
- [License: Unlicense](https://github.com/SuperCuber/dotter/blob/master/LICENSE)
- [README](https://github.com/SuperCuber/dotter/blob/master/README.md)
- [Releases](https://github.com/SuperCuber/dotter/releases)
- [SuperCuber/dotter on GitHub](https://github.com/SuperCuber/dotter)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/supercuber-dotter
