Introduction
dotling is a zero-dependency dotfiles management CLI written in Rust. It moves your config files into a central git repository and replaces them with symlinks (or copies). It handles the tedious parts — path mapping, conflict detection, encryption, templating, hooks, and multi-OS support — so you can set up a new machine in seconds.
Features
- Symlink & copy deployment — choose per file, switch anytime
- Bidirectional sync —
dotling syncpushes from repo to actual and pulls from actual to repo automatically - Automatic path mapping —
~/.config/nvimbecomesconfig/nvim,~/.zshrcbecomesshell/zshrc - Multi-OS support — tag entries as
linux,macos, orwindows; skip irrelevant files automatically - Secure password vault — encrypt sensitive files using Argon2id + ChaCha20-Poly1305
- Encrypted sync — sync handles encrypted entries in both directions; modified plaintext is re-encrypted back into the repo automatically
- Portable secrets — export your vault to easily unlock secrets on a new machine
- Native git integration — dotling manages the symlinks, you manage the repo with native
gitcommands - Dotfile templating — add machine-specific values using
{{ var.key }}syntax; render on every sync - Health checks —
dotling doctoraudits broken links, orphaned entries, and repo issues - Conflict-safe — refuses to overwrite unmanaged files without explicit confirmation
- Lifecycle hooks — run custom commands before/after syncing at repository or entry level with safe trust verification
- Interactive 3-way merge — cleanly merge changes between repo and local files with standard git-style conflict markers
- Fingerprint-based status — speed up sync checks using lightweight Blake2s-256 fingerprints
- Shell completions — tab-completion for bash, zsh, fish, elvish, and powershell
How it works
dotling moves your config files into a central git repository and replaces them with symlinks (or copies). Each tracked file is recorded in a dotling.toml config at the repo root.
Symlinks (default): the deployed file points to the repo — edits are instantly reflected in your repo. dotling sync ensures the symlink is present and correct.
Copies (--copy): the deployed file is a standalone copy. Useful for apps that don’t support symlinks. dotling sync compares content fingerprints and copies in whichever direction is newer.
Since dotling doesn’t wrap git, you use native git commands to commit, push, and pull your dotfiles repo.
Next steps
- Getting Started — install dotling and set up your first dotfiles repo
- Configuration — understand the
dotling.tomlformat - CLI Reference — explore all available commands
Getting Started
Installation
From crates.io (recommended)
cargo install dotling
Prebuilt binaries
Download a prebuilt binary from the latest GitHub release:
| Platform | Binary |
|---|---|
| Linux (x86_64, glibc) | dotling-x86_64-linux.tar.gz |
| Linux (x86_64, musl) | dotling-x86_64-linux-musl.tar.gz |
| Linux (aarch64) | dotling-aarch64-linux.tar.gz |
| macOS (Intel) | dotling-x86_64-macos.tar.gz |
| macOS (Apple Silicon) | dotling-aarch64-macos.tar.gz |
# Example: Linux x86_64
curl -fsSL https://github.com/auricvex/dotling/releases/latest/download/dotling-x86_64-linux.tar.gz \
| tar xz -C ~/.local/bin/
Homebrew (macOS & Linux)
brew tap auricvex/auricvex
brew install dotling
Nix
nix run github:auricvex/dotling
Quick Start
1. Initialize a dotfiles repo
# Create a new repo
dotling init ~/dotfiles
# Or clone an existing one
dotling init git@github.com:you/dotfiles.git
This creates the repo directory, initializes git, and writes a dotling.toml config file.
2. Track your config files
dotling add ~/.zshrc
dotling add ~/.config/nvim
dotling moves each file into the repo (organized by category) and deploys a symlink back to the original location. See Path Mapping for how files are organized.
3. Sync everything
dotling sync
This pushes repo files to their actual locations (creating or fixing symlinks) and pulls any copy-mode entries that were modified locally.
4. Commit and push
Since dotling doesn’t wrap git, use native commands:
cd ~/dotfiles
git add .
git commit -m "initial setup"
git push
5. Set up a new machine
# Clone your dotfiles repo
dotling init git@github.com:you/dotfiles.git
# Deploy everything
dotling sync
# If you have encrypted entries, import your vault first
dotling vault import my-vault.bundle
dotling sync
Next steps
- Configuration — learn about
dotling.tomland entry options - Templates — add machine-specific values to dotfiles
- Encryption — encrypt sensitive files with the vault
- CLI Reference — explore all commands and flags
Configuration
Tracked entries and settings are stored in dotling.toml at the repo root.
Example
# dotling.toml — managed by dotling, safe to hand-edit
[settings]
method = "symlink" # Default deployment method: "symlink" or "copy"
[hooks]
init = "echo 'Initializing repo...'"
before = "echo 'Starting global before-sync hook...'"
after = "echo 'Global after-sync hook completed.'"
[vars]
hostname = "my-mac" # shared default — override in ~/.dotling/vars.toml
primary_user = "user" # placeholder
[[entries]]
source = "shell/zshrc"
target = "~/.zshrc"
before = "echo 'Updating zshrc...'"
after = "echo 'zshrc updated!'"
[[entries]]
source = "config/nvim/init.lua"
target = "~/.config/nvim/init.lua"
method = "copy"
permissions = "0600"
[[entries]]
source = "shell/bashrc"
target = "~/.bashrc"
os = "linux"
Sections
[settings]
Global settings that apply to all entries unless overridden per-entry.
| Field | Type | Default | Description |
|---|---|---|---|
method | string | "symlink" | Default deployment method: "symlink" or "copy" |
[hooks]
Global lifecycle hooks that run at the beginning and end of dotling sync.
| Field | Description |
|---|---|
init | Command run during dotling init (also runs when adopting or cloning) |
before | Command run before any entries are synced |
after | Command run after all entries are successfully synced |
See Lifecycle Hooks for details on the hook trust system and environment variables.
[vars]
Shared template variable defaults. These are committed to git and serve as documentation and fallbacks. Machine-specific overrides live in ~/.dotling/vars.toml (never committed).
[vars]
hostname = "my-mac" # placeholder — override locally
primary_user = "user" # placeholder
See Templates for the full variable system.
[[entries]]
Each tracked file or directory is an entry. The order in dotling.toml determines the sync order.
Entry Fields
| Field | Type | Description |
|---|---|---|
source | string | Repo-relative path (required) |
target | string | Deploy target path with ~ support (required) |
method | string | Override: "symlink" or "copy" |
encrypted | boolean | true, 1, or yes — marks entry as encrypted |
directory | boolean | true, 1, or yes — marks entry as a directory |
template | boolean | true, 1, or yes — marks entry as a template |
os | string | "all" (default), "linux", "macos", or "windows" |
permissions | string | Octal permissions applied on sync, e.g. "0600" |
before | string | Entry-level hook run before sync |
after | string | Entry-level hook run after sync |
Note: Template entries are marked with
template: trueindotling.toml(set automatically bydotling add --template).
Path Mapping
Files are organized into categories automatically when added:
| Home path | Repo path |
|---|---|
~/.config/nvim/init.lua | config/nvim/init.lua |
~/.zshrc | shell/zshrc |
~/.bashrc | shell/bashrc |
~/.gitconfig | git/gitconfig |
~/.vimrc | vim/vimrc |
~/.tmux.conf | tmux/tmux.conf |
~/.ssh/config | ssh/config |
~/.gnupg/gpg.conf | gnupg/gpg.conf |
~/.somerc | home/somerc |
The mapping rules are:
.config/*files go toconfig/- Shell files (
.zshrc,.bashrc,.bash_profile,.profile,.zprofile,.zshenv,.fishrc) go toshell/ - Git files (
.gitconfig,.gitignore_global) go togit/ - Vim files (
.vimrc,.gvimrc) go tovim/ - Tmux files (
.tmux.conf) go totmux/ - SSH files (
.ssh/) go tossh/ - GnuPG files (
.gnupg/) go tognupg/ - Everything else goes to
home/
Multi-OS Support
Tag entries with --os to restrict them to a specific platform:
dotling add ~/.zshrc --os macos
dotling add ~/.bashrc --os linux
When deploying, dotling automatically skips entries that don’t match the current OS. Entries without an --os flag (or tagged all) deploy everywhere.
Platform aliases: darwin = macos, win = windows.
Templates
Some dotfiles contain machine-specific values — a hostname in a Nix flake, a username in a config, a path that differs per machine. dotling supports opt-in templating to handle this.
How it works
Any file tracked with --template is marked with template: true in dotling.toml. On every sync, dotling renders the template and writes the output to the deploy target — the repo source is never deployed directly.
# 1. Set your machine-local variables
dotling vars set hostname "Some hostname"
dotling vars set primary_user "someuser"
# 2. Add a file as a template
dotling add ~/.config/nix-darwin/flake.nix --template
# 3. On another machine, sync will detect missing vars and prompt for them
dotling sync
Template syntax
Variables are wrapped in double curly braces:
{{ var.key }}
{{ dotling.hostname }}
{{ env.VAR }}
Namespaces
| Expression | Description |
|---|---|
{{ var.key }} | User-defined variable (local or config default) |
{{ dotling.hostname }} | Current machine hostname |
{{ dotling.username }} | Current OS username |
{{ dotling.os }} | macos, linux, or windows |
{{ dotling.arch }} | x86_64, aarch64, or arm |
{{ dotling.home }} | Home directory path |
{{ dotling.repo }} | Dotfiles repo root path |
{{ env.VAR }} | Environment variable |
Filters
Filters are applied with the pipe (|) syntax:
{{ var.key | upper }} Convert to uppercase
{{ var.key | lower }} Convert to lowercase
{{ var.key | trim }} Strip leading/trailing whitespace
{{ var.key | quote }} Wrap in double quotes: "value"
{{ var.key | squote }} Wrap in single quotes: 'value'
{{ var.key | default "foo" }} Use fallback if variable is not set
Filters can be chained:
{{ var.name | trim | upper }}
Whitespace control
Use - to strip surrounding whitespace:
{{- var.key -}} Strip whitespace on both sides
{{- var.key }} Strip whitespace on the left only
{{ var.key -}} Strip whitespace on the right only
This is useful for controlling indentation in generated files:
{{- if var.enable_feature -}}
feature = true;
{{- end -}}
Scripts
Templates can execute inline shell commands and insert their standard output into the document using backticks inside template tags:
{{ `uname -s` | lower }}
{{ `curl -sSf https://api.ipify.org` | trim }}
- Interpreter: Commands are executed using
sh -con Unix andcmd /Con Windows. - Environment: Internal shell pipes, redirects, and environment variables work as expected (e.g.
{{ `echo $USER` }}). - Whitespace: A command’s output usually ends with a trailing newline. Dotling trims trailing whitespace automatically before the value is processed by filters, so it substitutes cleanly inline.
- Failures: A non-zero exit code or an execution failure fails the template render immediately. You can use the
`default`filter to catch command failures and provide a fallback value:{{ `brew --prefix` | default "/opt/homebrew" }}.
Script Security
Arbitrary command execution during config syncing is a security risk for shared dotfiles. Dotling reuses the HookSession trust system to protect against malicious template scripts.
When an untrusted script is encountered during dotling sync or dotling add, the user is interactively prompted to trust it. Trusted scripts are hashed and remembered in ~/.dotling/trusted_hooks. In non-interactive environments (e.g. CI), untrusted scripts are skipped (which fails the template render unless caught by a default filter) unless explicitly allowed by setting DOTLING_ALLOW_HOOKS=1.
Variable sources
Variables are resolved in priority order:
- Local store —
~/.dotling/vars.toml(machine-specific, never committed) - Config defaults —
[vars]indotling.toml(shared, committed)
Built-in variables (dotling.*) and environment variables (env.*) are separate namespaces resolved directly.
Shared defaults
Shared defaults in dotling.toml act as documentation and fallbacks — use placeholders, not real values:
# dotling.toml
[vars]
hostname = "my-mac" # placeholder — override in ~/.dotling/vars.toml
primary_user = "user" # placeholder
Machine-local variables
Machine-specific values are stored in ~/.dotling/vars.toml and are never committed to git:
dotling vars set hostname "work-laptop"
dotling vars set primary_user "alice"
dotling vars reference
dotling vars list # show all resolved variables
dotling vars set hostname "my-mac" # set a machine-local variable
dotling vars get hostname # print the resolved value
dotling vars unset hostname # remove from local store
dotling vars check # validate all templates for unresolved variables
dotling vars import ~/.env # bulk-import from .env or TOML file
dotling vars export # print local variables as TOML
Encrypted templates
Sensitive templates (e.g. a config containing tokens) can be both templated and encrypted:
dotling add ~/.config/secret.conf --template --encrypt
The pipeline on sync is: vault decrypt -> render with vars -> deploy.
Example
Given this template at config/nix-darwin/flake.nix:
{
description = "Nix darwin config for {{ var.hostname }}";
darwinConfigurations = {
"{{ var.hostname }}" = darwin.lib.darwinSystem {
system = "{{ var.arch }}-darwin";
modules = [ ./configuration.nix ];
};
};
}
And these local variables:
dotling vars set hostname "work-mbp"
The rendered output on an Apple Silicon Mac would be:
{
description = "Nix darwin config for work-mbp";
darwinConfigurations = {
"work-mbp" = darwin.lib.darwinSystem {
system = "aarch64-darwin";
modules = [ ./configuration.nix ];
};
};
}
Encryption
dotling includes a built-in portable encryption vault protected by Argon2id and ChaCha20-Poly1305. This lets you safely commit API keys, .env files, or ssh configs to your public dotfiles repo.
Vault architecture
The vault lives at ~/.dotling/vault/ and contains:
| File | Purpose |
|---|---|
identity.enc | Encrypted secret (your vault’s master key) |
config.toml | Vault metadata (creation date, version) |
Key derivation
Your vault password is processed through Argon2id with a 32-byte random salt, producing a 32-byte encryption key. This key is used to encrypt a randomly generated 32-byte identity secret with ChaCha20-Poly1305 (12-byte random nonce).
Encrypted file format
Each encrypted file uses this format:
DOTLING-ENC-V2
<12-byte nonce as hex>
<base64-encoded ciphertext + 16-byte Poly1305 auth tag>
The ciphertext is the file content encrypted with ChaCha20-Poly1305 using the vault’s master key.
Using encryption
1. Initialize your vault
dotling vault init
You’ll be prompted for a password (entered twice for confirmation).
2. Add a file with encryption
dotling add ~/.ssh/config --encrypt
dotling reads your local file, encrypts it, stores the ciphertext in your git repo, and deploys the decrypted file locally with secure permissions.
3. Sync encrypted entries
dotling sync handles encrypted entries in both directions:
# Edit your deployed file, then sync it back
vim ~/.ssh/config
dotling sync # detects the file is newer -> re-encrypts
| Direction | Trigger |
|---|---|
| Push (decrypt) | Source file newer or target missing |
| Pull (re-encrypt) | Target file newer |
4. Edit encrypted files
Use dotling edit to open an encrypted file in your $EDITOR without a manual decrypt/encrypt cycle:
dotling edit ~/.ssh/config
dotling edit ssh/config # also works with repo paths
The decrypted content is written to a secure temp file in ~/.dotling/tmp/ with mode 0600. Temp files are securely wiped (overwritten with zeros) before deletion.
Editor resolution: $DOTLING_EDITOR -> $VISUAL -> $EDITOR -> vim -> nano -> vi. GUI editors (code, subl, zed, pulsar, atom) automatically get --wait appended.
5. Encrypt and decrypt in-place
dotling encrypt <paths> # encrypt tracked entries
dotling decrypt <paths> # decrypt back to plaintext
These modify files in-place in the repo.
Migrating to a new machine
Export
From your old machine, export the vault as a single encrypted bundle:
dotling vault export my-vault.bundle
Import
On the new machine, import the bundle and sync:
dotling vault import my-vault.bundle
dotling sync
You’ll be prompted for your vault password during import.
Bundle format
The vault bundle is a single encrypted file:
DOTLVAUL (8-byte magic)
0x01 (1 byte: version)
<32-byte salt> (Argon2id salt)
<12-byte nonce> (ChaCha20-Poly1305 nonce)
<ciphertext + tag> (encrypted payload)
The encrypted payload contains the vault config and identity secret.
Other vault commands
dotling vault init # create a new vault
dotling vault show # display vault status and location
dotling vault export <path> # export as encrypted bundle
dotling vault import <path> # import a bundle
dotling vault change-password # change the vault password
Sync fingerprints
Previously, encrypted entries had to be decrypted to verify their sync state. dotling uses lightweight Blake2s-256 sync fingerprints stored in ~/.dotling/fingerprints.toml:
- After each successful sync, dotling records the content hashes of the encrypted ciphertext and the local plaintext target
- On subsequent
statusorsyncchecks, dotling compares current file hashes against the stored fingerprint - Benefit: You can run
dotling statusordotling sync --dry-runwithout entering your vault password. A password is only requested when actual file modifications need to be decrypted or re-encrypted.
Sync
dotling sync is the core command that keeps your repo and actual filesystem in sync. It handles symlinks, copies, encrypted entries, and templates — all in one pass.
Sync direction
dotling sync decides the direction per entry:
| Entry type | Push (repo -> actual) | Pull (actual -> repo) |
|---|---|---|
| Symlink | Create/fix symlink | Never (symlink always reads repo) |
| Copy | Source newer or target missing | Target newer |
| Encrypted | Source newer or target missing -> decrypt | Target newer -> re-encrypt |
| Template | Always renders and deploys | Never (template source is canonical) |
When both sides differ and timestamps are equal, dotling defaults to repo wins (push). Pass --prefer-actual to flip this.
Sync process
For each entry, dotling:
- Checks if the entry’s OS matches the current platform (skips if not)
- Determines the entry state:
Deployed,Modified,Missing,Broken, orConflict - Uses fingerprint comparison (for copy/encrypted) or mtime to decide direction
- If both sides changed, prompts for conflict resolution (unless
--forceor--no-interactive) - Executes the sync action (push or pull)
- Records fingerprints for future comparison
- Runs entry-level hooks (before/after)
Global hooks (before/after) run at the very beginning and end of the sync session.
Conflict resolution
When sync detects a conflict between the repository and your local target, you can choose:
| Option | Key | Description |
|---|---|---|
| Keep Local | k | Overwrite the repo with your local file (pulls to repo) |
| Use Repo | r | Overwrite the local file with the repo version (pushes to local) |
| Merge | m | Perform a three-way merge (see below) |
| Diff | d | Compare inline changes |
| Skip | s | Leave this entry unresolved and continue |
Three-way merge
The merge option performs a line-level three-way merge using the last-in-sync snapshot as the base, combining modifications from both the repo (ours) and local target (theirs). Non-overlapping changes are cleanly auto-merged, while overlapping conflicts are highlighted with standard git conflict markers:
<<<<<<< repo
repo version content
=======
actual local content
>>>>>>> actual
The merge outcome is written back to both the local disk and the repository.
Conflict types
- First seen — a file exists at the target path but was never tracked by dotling
- Both modified — both the repo and local file changed since the last sync
- Timestamp tie — files differ but modification times are identical
Snapshots
Snapshots used as the merge base are stored in ~/.dotling/snapshots/<source> and are updated after each successful sync of a copy-mode plain file.
Fingerprints
dotling uses Blake2s-256 content hashes stored in ~/.dotling/fingerprints.toml for change detection. This avoids decrypting encrypted files just to check if they’ve changed.
- After each successful sync, dotling records the content hashes of the source, target, and (if encrypted) the ciphertext
- On subsequent checks, dotling compares current hashes against stored fingerprints
- Benefit:
dotling statusanddotling sync --dry-runwork instantly without entering your vault password
Lifecycle Hooks
Global hooks
Global hooks run at the very beginning and very end of the dotling sync session:
| Hook | When it runs |
|---|---|
init | During dotling init (also runs when adopting or cloning) |
before | Before any entries are synced |
after | After all entries are successfully synced |
Entry-level hooks
Each entry can define its own hooks:
| Hook | When it runs |
|---|---|
before | Before this entry is pushed or pulled |
after | After this entry is successfully pushed or pulled |
Execution context
Hooks are executed in the repository root directory. The following environment variables are set:
| Variable | Description |
|---|---|
DOTLING_HOOK_TYPE | global_init, global_before, global_after, entry_before, entry_after |
DOTLING_REPO_ROOT | Absolute path to the dotfiles repository |
DOTLING_DRY_RUN | "true" if running with --dry-run, otherwise "false" |
DOTLING_ENTRY_SOURCE | (Entry hooks only) Repo-relative path of the entry’s source |
DOTLING_ENTRY_TARGET | (Entry hooks only) Target path of the entry’s deployed file |
DOTLING_ENTRY_ACTION | (Entry hooks only) Current action: "push" or "pull" |
Hook trust system
To protect against malicious code in imported dotfile repositories, dotling prompts for verification before running a hook for the first time:
⚡ Untrusted hook detected (type: entry_before):
echo "updating shell configuration"
? Do you want to run this hook? [y]es (once) / [n]o (skip) / [a]lways (trust) / [s]kip all >
Selecting always stores the Blake2s-256 hash of the command string in ~/.dotling/state/trusted_hooks.
- Pass
--allow-hooks(or setDOTLING_ALLOW_HOOKS=1) to auto-execute all hooks - Pass
--no-hooks(or setDOTLING_NO_HOOKS=1) to disable all hooks
Hook retry
If a hook exits with a non-zero status, dotling retries up to 3 times (1 initial + 2 retries) before aborting.
Flags
| Flag | Description |
|---|---|
--dry-run | Show what would change without modifying anything |
--force | Overwrite conflicting files without prompting (repo wins) |
--prefer-actual | When both sides differ, prefer the local file (alias: --prefer-local) |
--no-interactive | Skip conflicting entries and print a warning |
--allow-hooks | Execute all hooks without prompting |
--no-hooks | Disable all hook execution |
CLI Reference
dotling is organized as a set of subcommands. Each command has its own page with detailed usage, flags, and examples.
Commands
| Command | Description |
|---|---|
| dotling init | Initialize a new repo or clone an existing one |
| dotling add | Move files into the repo and deploy symlinks/copies |
| dotling remove | Untrack entries and restore files to original locations |
| dotling sync | Bidirectional sync between repo and actual filesystem |
| dotling status | Show deployment status of all tracked entries |
| dotling edit | Edit a tracked entry in $EDITOR |
| dotling encrypt | Encrypt tracked entries |
| dotling decrypt | Decrypt encrypted entries |
| dotling vault | Manage the encryption vault |
| dotling doctor | Audit repository health |
| dotling vars | Manage machine-local template variables |
| dotling completions | Generate shell completion scripts |
Global flags
| Flag | Description |
|---|---|
-v, --verbose | Show hints and additional details |
-h, --help | Print help |
-V, --version | Print version |
Key command flags
| Command | Flag | Description |
|---|---|---|
add | --copy | Deploy as a copy instead of a symlink |
add | --encrypt | Encrypt the file(s) using the vault password |
add | --template | Track as a template: rendered on each sync |
add | --os <platform> | Target OS: linux, macos, windows |
sync | --dry-run | Preview changes without modifying anything |
sync | --force | Overwrite conflicting files (repo wins) |
sync | --prefer-actual | Prefer the local file on conflict |
sync | --no-interactive | Skip conflicts, print warnings |
sync | --allow-hooks | Execute all hooks without prompting |
sync | --no-hooks | Disable all hook execution |
status | --diff | Show inline diffs for modified copy entries |
dotling init
Initialize a new dotfiles repo or adopt an existing one.
Usage
dotling init [PATH|URL]
Default: ~/dotfiles
Description
dotling init sets up a dotfiles repository. Depending on the argument:
- A local path — creates the directory, writes a default
dotling.toml, runsgit init, and registers the repo root - A git URL — clones the repo to
~/dotfiles, registers the repo root, and suggests runningdotling syncto deploy entries
After initialization, the repo root is stored in ~/.dotling/state.toml so dotling knows where to find it.
What happens
- Creates the repo directory (or clones from URL)
- Writes a default
dotling.tomlwith empty sections - Runs
git init(for new repos) - Registers the repo root in
~/.dotling/state.toml - Runs the
[hooks] initcommand if defined - For cloned repos: suggests running
dotling syncto deploy entries
Examples
# Create a new dotfiles repo
dotling init ~/dotfiles
# Create at the default location
dotling init
# Clone an existing repo
dotling init git@github.com:you/dotfiles.git
dotling init https://github.com/you/dotfiles.git
dotling add
Add files or directories to tracking.
Usage
dotling add <PATHS> [OPTIONS]
Arguments
| Argument | Description |
|---|---|
<PATHS> | One or more file or directory paths to track |
Options
| Flag | Description |
|---|---|
--encrypt | Encrypt the file(s) using the vault password |
--copy | Deploy as a copy instead of a symlink |
--template | Track as a template: rendered on each sync with machine-local variables |
--os <platform> | Restrict to a specific OS: linux, macos, windows |
Description
dotling add moves files from their original location into the repo and deploys a symlink (or copy) back. The file is recorded in dotling.toml with its source (repo-relative) and target (original) paths.
Automatic path mapping
Files are organized into categories in the repo. See Path Mapping for the full mapping rules.
Directories
When adding a directory, dotling tracks it as a single entry in dotling.toml with directory: true. The entire directory is moved into the repo and deployed as a unit (symlink or copy).
Encryption
With --encrypt, the file is encrypted using the vault’s master key before being stored in the repo. You’ll be prompted for your vault password if the vault is locked.
Templates
With --template, the source file is tracked as a template (using the template: true field in dotling.toml). On each sync, dotling renders the template with variables and writes the output to the target. See Templates for syntax details.
OS restriction
With --os, the entry is tagged for a specific platform and will only be deployed when the current OS matches.
Examples
# Track a single file
dotling add ~/.zshrc
# Track a directory
dotling add ~/.config/nvim
# Track with encryption
dotling add ~/.ssh/config --encrypt
# Track as a copy (not symlink)
dotling add ~/.config/some-app --copy
# Track as a template
dotling add ~/.config/nix-darwin/flake.nix --template
# Track for a specific OS
dotling add ~/.bashrc --os linux
# Combine flags
dotling add ~/.config/secret.conf --template --encrypt --os macos
dotling remove
Remove entries from tracking.
Usage
dotling remove <ENTRIES>
Arguments
| Argument | Description |
|---|---|
<ENTRIES> | Source paths, target paths, or partial matches of entries to remove |
Description
dotling remove undeploys tracked entries and restores files to their original locations. For each entry:
- Undeploy — removes the symlink (or copy) from the target path
- Restore — moves the file from the repo back to its original target location
- Decrypt — if the entry was encrypted, decrypts the file before restoring
- Clean up — removes empty parent directories in the repo
- Untrack — removes the entry from
dotling.toml
The restored file is placed at its original target path, so your config files remain intact after removing them from dotling.
Examples
# Remove by source path
dotling remove shell/zshrc
# Remove by target path
dotling remove ~/.zshrc
# Remove multiple entries
dotling remove ~/.zshrc ~/.bashrc
# Partial match works too
dotling remove zshrc
dotling sync
Synchronise tracked entries between the repo and the actual filesystem.
Usage
dotling sync [OPTIONS]
Options
| Flag | Description |
|---|---|
--dry-run | Show what would change without modifying anything |
--force | Overwrite conflicting files without prompting (repo wins) |
--prefer-actual | When both sides differ, prefer the local file (alias: --prefer-local) |
--no-interactive | Skip conflicting entries and print a warning |
--allow-hooks | Execute all hooks without prompting |
--no-hooks | Disable all hook execution |
Description
dotling sync is the core command that keeps your repo and actual filesystem in sync. It processes all entries in dotling.toml:
- Symlink entries — ensures the symlink exists and points to the correct repo file
- Copy entries — compares content fingerprints and copies in the newer direction
- Encrypted entries — decrypts or re-encrypts based on which side is newer
- Template entries — renders the template and deploys the output
See Sync for the full sync process, conflict resolution, and hook system.
Examples
# Sync everything
dotling sync
# Preview changes
dotling sync --dry-run
# Force repo version on all conflicts
dotling sync --force
# Prefer local files on conflicts
dotling sync --prefer-actual
# Non-interactive (for scripts/CI)
dotling sync --no-interactive
# Trust all hooks
dotling sync --allow-hooks
# Skip all hooks
dotling sync --no-hooks
dotling status
Show deployment status of all tracked entries.
Usage
dotling status [OPTIONS]
Options
| Flag | Description |
|---|---|
--diff | Show inline diffs for modified copy entries |
Description
dotling status displays the current state of each tracked entry, grouped by category. It checks whether each entry is properly deployed, up to date, or has issues.
Status indicators
| Status | Meaning |
|---|---|
Deployed | Entry is correctly deployed and in sync |
Modified | Entry has local changes that differ from the repo |
Missing | Target file is missing |
Broken | Symlink points to a non-existent target |
Conflict | Both repo and local file have changed |
Sync badges
| Badge | Meaning |
|---|---|
[in sync] | Repository and target match |
[needs sync] | One side has changed, run dotling sync |
[diff] | Content differs (shown with --diff) |
Fingerprints
For copy-mode and encrypted entries, status checks use Blake2s-256 fingerprints stored in ~/.dotling/fingerprints.toml. This means you can check status without entering your vault password — it’s only needed when actual file modifications are required.
Examples
# Show status of all entries
dotling status
# Show inline diffs for modified entries
dotling status --diff
dotling edit
Edit a tracked entry in your $EDITOR.
Usage
dotling edit <ENTRY>
Arguments
| Argument | Description |
|---|---|
<ENTRY> | Source path, target path, or partial match of the entry to edit |
Description
dotling edit opens a tracked entry in your preferred text editor. It accepts any identifier that uniquely matches an entry: the repo source path, the target path, or a partial name match.
Plain and template entries
For plain and template entries, the repo source file is opened directly. Changes are saved to the repo — run dotling sync to deploy them.
Encrypted entries
For encrypted entries, dotling:
- Decrypts the file to a secure temp file in
~/.dotling/tmp/(mode0600) - Opens the temp file in your editor
- Re-encrypts the content and writes it back to the repo
- Securely wipes (overwrites with zeros) and deletes the temp file
Editor resolution
The editor is resolved in this order:
$DOTLING_EDITOR— highest priority$VISUAL— standard Unix convention$EDITOR— standard fallbackvim->nano->vi— hardcoded fallbacks
GUI editors (code, subl, zed, pulsar, atom) automatically get --wait appended so dotling waits for the editor to close before re-encrypting.
Examples
# Edit by target path
dotling edit ~/.ssh/config
# Edit by source path
dotling edit ssh/config
# Partial match
dotling edit zshrc
# Use a specific editor
DOTLING_EDITOR=nvim dotling edit ~/.gitconfig
dotling encrypt
Encrypt tracked entries in-place in the repo.
Usage
dotling encrypt <PATHS>
Arguments
| Argument | Description |
|---|---|
<PATHS> | Source paths, target paths, or partial matches of entries to encrypt |
Description
dotling encrypt encrypts tracked entries using the vault’s master key. The plaintext file in the repo is replaced with an encrypted file in-place. You’ll be prompted for your vault password if the vault is locked.
Directories
When encrypting a directory entry, each file within it is encrypted individually.
Examples
# Encrypt by source path
dotling encrypt ssh/config
# Encrypt by target path
dotling encrypt ~/.ssh/config
# Encrypt multiple entries
dotling encrypt ssh/config gnupg/gpg.conf
dotling decrypt
Decrypt encrypted entries back to plaintext in the repo.
Usage
dotling decrypt <PATHS>
Arguments
| Argument | Description |
|---|---|
<PATHS> | Source paths, target paths, or partial matches of entries to decrypt |
Description
dotling decrypt decrypts encrypted entries and replaces the encrypted files with plaintext in the repo. You’ll be prompted for your vault password if the vault is locked. The entry’s encrypted flag is removed from dotling.toml.
Examples
# Decrypt by source path
dotling decrypt ssh/config
# Decrypt by target path
dotling decrypt ~/.ssh/config
# Decrypt multiple entries
dotling decrypt ssh/config gnupg/gpg.conf
dotling vault
Manage the encryption vault.
Usage
dotling vault <ACTION>
Actions
dotling vault init
Initialize a new vault with a password.
dotling vault init
You’ll be prompted for a password (entered twice for confirmation). This creates the vault at ~/.dotling/vault/ with an encrypted identity secret.
dotling vault show
Display vault status and location.
dotling vault show
Shows whether a vault exists and its location. Note: creation date is not currently displayed.
dotling vault export
Export the vault as a portable encrypted bundle.
dotling vault export <PATH>
Creates a single encrypted file containing the vault config and identity secret. Use this to migrate your vault to a new machine.
dotling vault import
Import a vault bundle.
dotling vault import <PATH>
Imports the vault from an encrypted bundle. You’ll be prompted for the bundle’s password. This overwrites any existing vault.
dotling vault change-password
Change the vault password.
dotling vault change-password
You’ll be prompted for the current password and a new password. The vault identity is re-encrypted with the new password.
Examples
# Set up encryption for the first time
dotling vault init
# Check vault status
dotling vault show
# Migrate to a new machine
dotling vault export ~/vault.bundle
# ... copy vault.bundle to new machine ...
dotling vault import ~/vault.bundle
# Change your password
dotling vault change-password
See also
- Encryption — full encryption guide
dotling doctor
Audit repository health and report issues.
Usage
dotling doctor
Description
dotling doctor runs a comprehensive health check on your dotfiles repository and reports any issues found. It checks:
- Repository existence — whether the repo root exists and is valid
- Config validity — whether
dotling.tomlparses correctly - Entry states — broken symlinks, missing source files, missing targets
- Template variables — unresolved variables in template entries
- Variable defaults — config defaults that look like real values (not placeholders)
- Git initialization — whether the repo is a git repository
- Vault initialization — whether the vault is set up
- Orphaned files — files in the repo that aren’t tracked by any entry in
dotling.toml
Examples
dotling doctor
Sample output:
[ok] Repo root: ~/dotfiles
[ok] Config: dotling.toml (3 entries)
[ok] Git: initialized
[warn] Vault: not initialized (run `dotling vault init`)
[ok] Entry: shell/zshrc -> ~/.zshrc
[err] Entry: config/nvim -> ~/.config/nvim (broken symlink)
[ok] Entry: ssh/config -> ~/.ssh/config (encrypted)
dotling vars
Manage machine-local template variables.
Usage
dotling vars <ACTION>
Actions
dotling vars list
Show all resolved variables (built-in, config defaults, and local).
dotling vars list
Variables are tagged with their source: [auto] for built-ins, [local] for machine-local, [default] for config defaults.
dotling vars set
Set a machine-local variable in ~/.dotling/vars.toml.
dotling vars set <KEY> <VALUE>
dotling vars get
Print the resolved value of a single variable.
dotling vars get <KEY>
Checks built-ins first, then local store, then config defaults.
dotling vars unset
Remove a variable from the local store.
dotling vars unset <KEY>
dotling vars check
Validate all template entries for unresolved variables.
dotling vars check
Reports any template variables that don’t have a value in either the local store or config defaults.
dotling vars import
Bulk-import variables from a TOML or .env file.
dotling vars import <PATH>
Accepts either a TOML file with a [vars] section or a .env file with KEY=VALUE pairs.
dotling vars export
Print local variables as TOML (useful for migrating to a new machine).
dotling vars export
Output can be redirected to a file and imported on another machine with dotling vars import.
Variable resolution priority
- Local store —
~/.dotling/vars.toml(machine-specific, never committed) - Config defaults —
[vars]indotling.toml(shared, committed)
Built-in variables (dotling.*) and environment variables (env.*) are separate namespaces.
Examples
# Set machine-specific values
dotling vars set hostname "work-laptop"
dotling vars set primary_user "alice"
# Check what's configured
dotling vars list
dotling vars get hostname
# Validate templates
dotling vars check
# Migrate to new machine
dotling vars export > vars-backup.toml
# ... on new machine ...
dotling vars import vars-backup.toml
# Import from .env file
dotling vars import ~/.env
See also
- Templates — full template syntax and variable system
dotling completions
Generate shell completion scripts.
Usage
dotling completions <SHELL>
Arguments
| Argument | Description |
|---|---|
<SHELL> | The shell to generate completions for |
Supported shells
bash, zsh, fish, elvish, powershell
Installation
Quick install (auto-detect shell)
just install-completions
Manual install
Bash
dotling completions bash > ~/.local/share/bash-completion/completions/dotling
Zsh
dotling completions zsh > ~/.zfunc/_dotling
Add to ~/.zshrc if not already present:
fpath=(~/.zfunc $fpath)
autoload -Uz compinit && compinit
Fish
dotling completions fish > ~/.config/fish/completions/dotling.fish
Elvish
dotling completions elvish > ~/.config/elvish/completions/dotling.elv
PowerShell
dotling completions powershell > dotling.ps1
Then source from your $PROFILE.
Examples
# Generate bash completions
dotling completions bash > ~/.local/share/bash-completion/completions/dotling
# Generate zsh completions
dotling completions zsh > ~/.zfunc/_dotling
# Generate fish completions
dotling completions fish > ~/.config/fish/completions/dotling.fish
Environment Variables
dotling reads and respects the following environment variables.
User-facing variables
| Variable | Description |
|---|---|
NO_COLOR | Disables ANSI color output when set (follows the no-color.org standard) |
DOTLING_EDITOR | Highest-priority editor override for dotling edit |
VISUAL | Editor override (standard Unix convention) |
EDITOR | Editor fallback |
DOTLING_ALLOW_HOOKS | Set to 1 or true to auto-trust and execute all hooks without prompting |
DOTLING_NO_HOOKS | Set to 1 or true to completely disable hook execution |
HOSTNAME | Fallback hostname for dotling.hostname built-in if the syscall fails (Unix/Linux/macOS) |
COMPUTERNAME | Fallback hostname for dotling.hostname built-in on Windows |
USER / USERNAME | Used for dotling.username built-in (USERNAME is the Windows fallback) |
Hook context variables
These variables are set in the environment when hooks execute:
| Variable | Description |
|---|---|
DOTLING_HOOK_TYPE | Type of hook: global_init, global_before, global_after, entry_before, entry_after |
DOTLING_REPO_ROOT | Absolute path to the dotfiles repository |
DOTLING_DRY_RUN | "true" if running with --dry-run, otherwise "false" |
DOTLING_ENTRY_SOURCE | (Entry hooks only) Repo-relative path of the entry’s source file/folder |
DOTLING_ENTRY_TARGET | (Entry hooks only) Target path of the entry’s deployed file/folder |
DOTLING_ENTRY_ACTION | (Entry hooks only) Current action being performed: "push" or "pull" |
Template environment variables
Any environment variable can be accessed in templates using the {{ env.VAR }} syntax. For example, {{ env.HOME }} resolves to the value of $HOME.
Global State
dotling stores all state under ~/.dotling/. This directory is never committed to git — it’s machine-local.
Directory layout
~/.dotling/
state.toml Registered repo root path
vars.toml Machine-local template variables
fingerprints.toml Sync fingerprint store (Blake2s-256 hashes)
vault/
identity.enc Encrypted vault secret
config.toml Vault metadata
snapshots/
<source> Plaintext snapshots for three-way merge base
state/
trusted_hooks Blake2s-256 hashes of trusted hook commands
tmp/ Secure temp files for encrypted file editing
File details
state.toml
Stores the path to the dotfiles repository:
repo = "/home/user/dotfiles"
vars.toml
Machine-local template variables. These override [vars] defaults in dotling.toml and are never committed to git:
[vars]
hostname = "work-laptop"
primary_user = "alice"
Managed via dotling vars set/get/unset/list.
fingerprints.toml
Sync fingerprint store for change detection. Uses Blake2s-256 content hashes so dotling can check sync state without decrypting files:
[[entries]]
source = "ssh/config"
enc_hash = "a1b2c3..."
target_hash = "d4e5f6..."
source_hash = "g7h8i9..."
vault/identity.enc
The vault’s encrypted identity secret. Format:
DOTLING-VAULT-V1
<32-byte salt as hex>
<12-byte nonce as hex>
<base64-encoded ciphertext + auth tag>
vault/config.toml
Vault metadata:
[vault]
version = 1
created = "2024-01-15T10:30:00Z"
snapshots/<source>
Plaintext snapshots of copy-mode entries used as the merge base for three-way merge. One file per tracked entry, named by the source path.
state/trusted_hooks
Blake2s-256 hashes of hook commands that the user has approved. One hash per line. When a hook is encountered, its hash is checked against this file to determine if it can run without prompting.
tmp/
Secure temporary files created during dotling edit for encrypted entries. Files are created with mode 0600 and securely wiped (overwritten with zeros) before deletion.
Architecture
dotling is structured as a layered Rust application with a command pattern. Each CLI subcommand maps to a module in src/commands/, and core logic is organized into four top-level modules.
Module hierarchy
dotling
├── core/ Foundational utilities
│ ├── error Error enum and Result type alias
│ ├── fs Filesystem helpers (walk, copy, symlink, atomic write)
│ ├── path Path mapping, tilde expansion, category rules
│ ├── platform OS detection, platform matching
│ └── store Global state at ~/.dotling/
│
├── config/ Data model and rendering
│ ├── mod.rs Config, Entry, Settings, Hooks, TOML parser/serializer
│ ├── template Template engine: {{ var.x }}, {{ dotling.x }}, {{ env.X }}
│ └── vars Machine-local variable store
│
├── crypto/ Encryption
│ ├── mod.rs ChaCha20-Poly1305 encryption, Argon2id key derivation
│ └── vault Vault management: init, unlock, export, import
│
├── sync/ Sync and deployment
│ ├── mod.rs Sync orchestration
│ ├── deploy Entry deployment: symlink/copy, state checking
│ ├── fingerprint Blake2s-256 content hashing for change detection
│ ├── hooks Lifecycle hooks with trust verification
│ └── merge Line-level three-way merge using LCS diff
│
├── cli.rs clap derive definitions for all CLI args/subcommands
├── commands/ Command handlers (one module per subcommand)
│ ├── init, add, remove, sync, status, edit
│ ├── encrypt, vault, doctor, vars, completions
│
├── ui.rs Terminal UI: colors, prompts, diff display
└── main.rs Entry point: parse CLI, dispatch, handle errors
Data flow
CLI input (clap)
→ main.rs (parse args, dispatch)
→ commands/<subcommand>::run()
→ core/store (load repo root from ~/.dotling/state.toml)
→ config/mod (load dotling.toml)
→ sync engine
→ sync/deploy (create/fix symlinks, copy files)
→ sync/fingerprint (record/check content hashes)
→ sync/hooks (execute lifecycle hooks)
→ sync/merge (three-way merge for conflicts)
→ crypto (encrypt/decrypt as needed)
→ config/template (render templates)
→ ui (display results)
Data model
Config (dotling.toml)
The central configuration file at the repo root. Contains:
Settings— global defaults (deployment method)Vec<Entry>— all tracked files/directoriesHooks— global lifecycle hooksVec<(String, String)>— shared variable defaults
The TOML parser and serializer are hand-rolled (no serde dependency).
Entry
Each tracked file or directory:
| Field | Type | Description |
|---|---|---|
source | String | Repo-relative path |
target | String | Deploy target path with ~ |
method | Option<DeployMethod> | Symlink or Copy override |
encrypted | bool | Whether the entry is encrypted |
directory | bool | Whether the entry is a directory |
template | bool | Whether the entry is a template |
os | Option<String> | Platform restriction |
permissions | Option<u32> | Octal permissions |
before | Option<String> | Pre-sync hook |
after | Option<String> | Post-sync hook |
FingerprintStore (~/.dotling/fingerprints.toml)
Maps source paths to EntryFingerprint records with three Blake2s-256 hashes: enc_hash (ciphertext), target_hash (deployed file), source_hash (repo source).
VarStore (~/.dotling/vars.toml)
Ordered list of (key, value) pairs for machine-local template variables.
Dependencies
| Crate | Purpose |
|---|---|
clap | CLI argument parsing with derive macros |
clap_complete | Shell completion generation |
chacha20poly1305 | AEAD encryption |
argon2 | Password-based key derivation |
blake2 | Content hashing (fingerprints, hook trust) |
rand | Cryptographic random number generation |
base64 | Binary-to-text encoding for encrypted files |
No serde. No async runtime. Minimal by design.
Testing
Tests are inline #[cfg(test)] mod tests blocks across 15 files (94 tests total). Uses tempfile for temporary directories. Tests that mutate environment variables use a shared Mutex guard pattern to prevent interference.
API Documentation
This section documents the public Rust API of the dotling crate, similar to rustdoc output. dotling is primarily a CLI tool, but its modules can be used as a library for building custom dotfile management workflows.
Module overview
| Module | Description |
|---|---|
| core | Error types, filesystem helpers, path mapping, platform detection, state store |
| config | Configuration data model, TOML parser, template engine, variable store |
| crypto | ChaCha20-Poly1305 encryption, Argon2id key derivation, vault management |
| sync | Entry deployment, fingerprint tracking, lifecycle hooks, three-way merge |
Re-exports
The crate root re-exports key types:
#![allow(unused)]
fn main() {
pub use error::{Error, Result};
pub use core::{error, fs, path, platform, store};
pub use config::{template, vars};
pub use sync::{deploy, fingerprint, hooks, merge};
}
core
Foundational utilities for error handling, filesystem operations, path mapping, platform detection, and global state management.
core::error
Result<T>
#![allow(unused)]
fn main() {
pub type Result<T> = std::result::Result<T, Error>;
}
Convenience alias for std::result::Result<T, dotling::Error>.
Error
#![allow(unused)]
fn main() {
pub enum Error {
Io {
path: PathBuf,
operation: &'static str,
source: io::Error,
},
Config {
message: String,
line: Option<usize>,
},
Crypto(String),
Deploy {
entry: String,
message: String,
},
Vault(String),
Template {
source: String,
message: String,
},
User(String),
}
}
Unified error type for all dotling operations.
| Variant | Usage |
|---|---|
Io | Filesystem errors with path and operation context |
Config | Configuration parsing errors with optional line number |
Crypto | Encryption/decryption failures |
Deploy | Deployment errors with entry name |
Vault | Vault operation failures |
Template | Template rendering errors with source file |
User | User-facing errors (e.g., cancelled prompts) |
Error::io()
#![allow(unused)]
fn main() {
pub fn io(path: impl Into<PathBuf>, operation: &'static str, source: io::Error) -> Self
}
Constructs an Error::Io variant.
core::fs
Filesystem helper functions. All operations are atomic where possible (write to temp, then rename).
walk_dir()
#![allow(unused)]
fn main() {
pub fn walk_dir(root: &Path, include_hidden: bool) -> Result<Vec<PathBuf>>
}
Recursively walks a directory and returns all file paths. When include_hidden is false, skips files and directories starting with ..
copy_file()
#![allow(unused)]
fn main() {
pub fn copy_file(src: &Path, dst: &Path) -> Result<()>
}
Copies a file from src to dst, creating parent directories as needed.
create_symlink()
#![allow(unused)]
fn main() {
pub fn create_symlink(target: &Path, link: &Path) -> Result<()>
}
Creates a symbolic link at link pointing to target. Creates parent directories as needed.
remove_symlink()
#![allow(unused)]
fn main() {
pub fn remove_symlink(path: &Path) -> Result<()>
}
Removes a symbolic link without following it.
atomic_write()
#![allow(unused)]
fn main() {
pub fn atomic_write(path: &Path, data: &[u8]) -> Result<()>
}
Writes data to a file atomically by writing to a temporary file in the same directory, then renaming.
is_symlink()
#![allow(unused)]
fn main() {
pub fn is_symlink(path: &Path) -> bool
}
Returns true if the path is a symbolic link.
read_link()
#![allow(unused)]
fn main() {
pub fn read_link(path: &Path) -> Result<PathBuf>
}
Reads the target of a symbolic link.
files_identical()
#![allow(unused)]
fn main() {
pub fn files_identical(a: &Path, b: &Path) -> Result<bool>
}
Compares two files byte-by-byte and returns true if they are identical.
cleanup_empty_parents()
#![allow(unused)]
fn main() {
pub fn cleanup_empty_parents(path: &Path, stop_at: &Path) -> Result<()>
}
Removes empty parent directories up to (but not including) stop_at.
set_permissions() (Unix only)
#![allow(unused)]
fn main() {
pub fn set_permissions(path: &Path, mode: u32) -> Result<()>
}
Sets Unix file permissions using an octal mode (e.g., 0o600).
get_permissions() (Unix only)
#![allow(unused)]
fn main() {
pub fn get_permissions(path: &Path) -> Result<Option<u32>>
}
Returns the Unix file permissions as an octal mode, or None on non-Unix platforms.
core::path
Path mapping and tilde expansion utilities.
home_dir()
#![allow(unused)]
fn main() {
pub fn home_dir() -> Result<PathBuf>
}
Returns the user’s home directory.
expand_tilde()
#![allow(unused)]
fn main() {
pub fn expand_tilde(path: &Path) -> Result<PathBuf>
}
Expands ~ at the start of a path to the user’s home directory.
collapse_tilde()
#![allow(unused)]
fn main() {
pub fn collapse_tilde(path: &Path) -> PathBuf
}
Replaces the home directory prefix with ~ in a path.
relative_to()
#![allow(unused)]
fn main() {
pub fn relative_to(target: &Path, base: &Path) -> Option<PathBuf>
}
Returns target as a relative path from base, or None if not possible.
map_to_repo()
#![allow(unused)]
fn main() {
pub fn map_to_repo(home_path: &Path) -> Result<PathBuf>
}
Maps a home directory path to its repo-relative path using category rules:
| Pattern | Repo path |
|---|---|
.config/* | config/* |
Shell files (.zshrc, .bashrc, etc.) | shell/ |
Git files (.gitconfig, etc.) | git/ |
Vim files (.vimrc, .vim/) | vim/ |
Tmux files (.tmux.conf, etc.) | tmux/ |
SSH files (.ssh/) | ssh/ |
GnuPG files (.gnupg/) | gnupg/ |
| Everything else | home/ |
resolve()
#![allow(unused)]
fn main() {
pub fn resolve(path: &Path) -> Result<PathBuf>
}
Resolves a path to an absolute path, expanding ~ and resolving . and ...
core::platform
Platform detection and matching.
Platform
#![allow(unused)]
fn main() {
pub enum Platform {
Linux,
Macos,
Windows,
}
}
Platform::current()
#![allow(unused)]
fn main() {
pub fn current() -> Self
}
Returns the current platform at compile time.
Platform::parse()
#![allow(unused)]
fn main() {
pub fn parse(s: &str) -> Option<Self>
}
Parses a platform string. Accepts: "linux", "macos" / "darwin", "windows" / "win".
Platform::as_str()
#![allow(unused)]
fn main() {
pub fn as_str(self) -> &'static str
}
Returns the platform as a lowercase string: "linux", "macos", or "windows".
should_deploy()
#![allow(unused)]
fn main() {
pub fn should_deploy(os: Option<&str>) -> bool
}
Returns true if an entry with the given os tag should be deployed on the current platform. None or "all" means deploy everywhere.
core::store
Global state management at ~/.dotling/.
state_dir()
#![allow(unused)]
fn main() {
pub fn state_dir() -> Result<PathBuf>
}
Returns the path to ~/.dotling/. Creates it if it doesn’t exist.
fingerprint_path()
#![allow(unused)]
fn main() {
pub fn fingerprint_path() -> Result<PathBuf>
}
Returns the path to ~/.dotling/fingerprints.toml.
vars_path()
#![allow(unused)]
fn main() {
pub fn vars_path() -> Result<PathBuf>
}
Returns the path to ~/.dotling/vars.toml.
snapshot_dir()
#![allow(unused)]
fn main() {
pub fn snapshot_dir() -> Result<PathBuf>
}
Returns the path to ~/.dotling/snapshots/. Creates it if it doesn’t exist.
snapshot_path()
#![allow(unused)]
fn main() {
pub fn snapshot_path(source: &str) -> Result<PathBuf>
}
Returns the path to a specific snapshot file for the given source entry.
get_repo_root()
#![allow(unused)]
fn main() {
pub fn get_repo_root() -> Result<Option<PathBuf>>
}
Returns the registered repo root from ~/.dotling/state.toml, or None if not set.
set_repo_root()
#![allow(unused)]
fn main() {
pub fn set_repo_root(repo_root: &Path) -> Result<()>
}
Registers the repo root in ~/.dotling/state.toml.
require_repo_root()
#![allow(unused)]
fn main() {
pub fn require_repo_root() -> Result<PathBuf>
}
Returns the repo root, or returns an error if not registered.
config_path()
#![allow(unused)]
fn main() {
pub fn config_path(repo_root: &Path) -> PathBuf
}
Returns the path to dotling.toml within the given repo root.
config
Configuration data model, hand-rolled TOML parser/serializer, template engine, and variable store.
config
DeployMethod
#![allow(unused)]
fn main() {
pub enum DeployMethod {
Symlink,
Copy,
}
}
Deployment method for an entry.
DeployMethod::as_str()
#![allow(unused)]
fn main() {
pub fn as_str(self) -> &'static str
}
Returns "symlink" or "copy".
Entry
#![allow(unused)]
fn main() {
pub struct Entry {
pub source: String,
pub target: String,
pub method: Option<DeployMethod>,
pub encrypted: bool,
pub directory: bool,
pub template: bool,
pub os: Option<String>,
pub permissions: Option<u32>,
pub before: Option<String>,
pub after: Option<String>,
}
}
A tracked file or directory entry.
| Field | Description |
|---|---|
source | Repo-relative path |
target | Deploy target path with ~ support |
method | Override for the default deployment method |
encrypted | Whether the entry is encrypted |
directory | Whether the entry is a directory |
template | Whether the entry is a template |
os | Platform restriction ("linux", "macos", "windows", or None for all) |
permissions | Octal permissions applied on sync |
before | Entry-level pre-sync hook |
after | Entry-level post-sync hook |
Settings
#![allow(unused)]
fn main() {
pub struct Settings {
pub method: DeployMethod,
}
}
Global settings. Default method is Symlink.
Hooks
#![allow(unused)]
fn main() {
pub struct Hooks {
pub init: Option<String>,
pub before: Option<String>,
pub after: Option<String>,
}
}
Global lifecycle hooks.
Config
#![allow(unused)]
fn main() {
pub struct Config {
pub settings: Settings,
pub entries: Vec<Entry>,
pub hooks: Hooks,
pub vars: Vec<(String, String)>,
}
}
The root configuration structure, parsed from dotling.toml.
Config::new()
#![allow(unused)]
fn main() {
pub fn new(path: PathBuf) -> Self
}
Creates a new empty config with default settings.
Config::load()
#![allow(unused)]
fn main() {
pub fn load(path: &Path) -> Result<Self>
}
Loads and parses a dotling.toml file.
Config::save()
#![allow(unused)]
fn main() {
pub fn save(&self) -> Result<()>
}
Serializes and writes the config back to dotling.toml.
Config::add_entry()
#![allow(unused)]
fn main() {
pub fn add_entry(&mut self, entry: Entry) -> Result<()>
}
Adds an entry to the config. Returns an error if an entry with the same source already exists.
Config::remove_entry()
#![allow(unused)]
fn main() {
pub fn remove_entry(&mut self, source: &str) -> Option<Entry>
}
Removes and returns the entry matching the given source path.
Config::find_entry()
#![allow(unused)]
fn main() {
pub fn find_entry(&self, query: &str) -> Option<&Entry>
}
Finds an entry by source path, target path, or partial match.
Config::find_entry_mut()
#![allow(unused)]
fn main() {
pub fn find_entry_mut(&mut self, query: &str) -> Option<&mut Entry>
}
Mutable version of find_entry.
config::template
TemplateVar
#![allow(unused)]
fn main() {
pub struct TemplateVar {
pub raw: String,
pub namespace: String,
pub key: String,
}
}
A parsed template variable reference.
| Field | Description |
|---|---|
raw | The full expression (e.g., "var.hostname") |
namespace | The namespace: "dotling", "var", or "env" |
key | The variable key within the namespace |
RenderContext
#![allow(unused)]
fn main() {
pub struct RenderContext {
pub builtins: HashMap<String, String>,
pub vars: Vec<(String, String)>,
pub env: HashMap<String, String>,
}
}
Context for template rendering.
RenderContext::new()
#![allow(unused)]
fn main() {
pub fn new(
repo_root: &str,
config_vars: &[(String, String)],
local_vars: &[(String, String)],
) -> Self
}
Creates a new render context. Built-in variables (dotling.*) are auto-populated. Local vars override config defaults.
RenderContext::resolve()
#![allow(unused)]
fn main() {
pub fn resolve(&self, namespace: &str, key: &str) -> Option<String>
}
Resolves a variable by namespace and key.
render()
#![allow(unused)]
fn main() {
pub fn render(template_text: &str, ctx: &RenderContext, source_name: &str) -> Result<String>
}
Renders a template string with the given context. Returns an error if any variable cannot be resolved (unless a default filter is used).
scan_variables()
#![allow(unused)]
fn main() {
pub fn scan_variables(template_text: &str) -> Vec<TemplateVar>
}
Scans a template string and returns all variable references found.
config::vars
VarStore
#![allow(unused)]
fn main() {
pub struct VarStore { /* private */ }
}
Machine-local variable store backed by ~/.dotling/vars.toml.
VarStore::load()
#![allow(unused)]
fn main() {
pub fn load() -> Result<Self>
}
Loads the variable store from ~/.dotling/vars.toml.
VarStore::save()
#![allow(unused)]
fn main() {
pub fn save(&self) -> Result<()>
}
Saves the variable store to disk.
VarStore::get()
#![allow(unused)]
fn main() {
pub fn get(&self, key: &str) -> Option<&str>
}
Returns the value for a key, or None.
VarStore::set()
#![allow(unused)]
fn main() {
pub fn set(&mut self, key: &str, value: &str)
}
Sets a key-value pair. Updates the value if the key already exists.
VarStore::remove()
#![allow(unused)]
fn main() {
pub fn remove(&mut self, key: &str) -> bool
}
Removes a key. Returns true if the key existed.
VarStore::iter()
#![allow(unused)]
fn main() {
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)>
}
Iterates over all key-value pairs.
VarStore::as_pairs()
#![allow(unused)]
fn main() {
pub fn as_pairs(&self) -> Vec<(String, String)>
}
Returns all pairs as owned (String, String) tuples.
VarStore::is_empty()
#![allow(unused)]
fn main() {
pub fn is_empty(&self) -> bool
}
Returns true if the store has no entries.
VarStore::len()
#![allow(unused)]
fn main() {
pub fn len(&self) -> usize
}
Returns the number of entries.
VarStore::path()
#![allow(unused)]
fn main() {
pub fn path() -> Result<PathBuf>
}
Returns the path to ~/.dotling/vars.toml.
import_from_file()
#![allow(unused)]
fn main() {
pub fn import_from_file(store: &mut VarStore, path: &Path) -> Result<usize>
}
Bulk-imports variables from a TOML file (with [vars] section) or .env file. Returns the number of variables imported.
looks_like_real_value()
#![allow(unused)]
fn main() {
pub fn looks_like_real_value(key: &str, value: &str, local_store: &VarStore) -> Option<String>
}
Heuristic check for whether a config default looks like a real value (not a placeholder). Returns a warning message if it does.
crypto
ChaCha20-Poly1305 encryption with Argon2id key derivation, and vault management.
crypto
encrypt_with_key()
#![allow(unused)]
fn main() {
pub fn encrypt_with_key(data: &[u8], key: &[u8; 32]) -> Result<Vec<u8>>
}
Encrypts data using ChaCha20-Poly1305 with the given 32-byte key. Generates a random 12-byte nonce. Returns the encrypted data in the DOTLING-ENC-V2 format:
DOTLING-ENC-V2
<12-byte nonce as hex>
<base64-encoded ciphertext + 16-byte auth tag>
decrypt_with_key()
#![allow(unused)]
fn main() {
pub fn decrypt_with_key(data: &[u8], key: &[u8; 32]) -> Result<Vec<u8>>
}
Decrypts data in the DOTLING-ENC-V2 format using the given 32-byte key. Returns the plaintext. Fails if the authentication tag is invalid (data tampered or wrong key).
is_encrypted_content()
#![allow(unused)]
fn main() {
pub fn is_encrypted_content(data: &[u8]) -> bool
}
Returns true if the data starts with the DOTLING-ENC-V2 header.
crypto::vault
vault_dir()
#![allow(unused)]
fn main() {
pub fn vault_dir() -> Result<PathBuf>
}
Returns the path to ~/.dotling/vault/. Creates it if it doesn’t exist.
vault_exists()
#![allow(unused)]
fn main() {
pub fn vault_exists() -> bool
}
Returns true if a vault has been initialized (both identity.enc and config.toml exist).
init_vault()
#![allow(unused)]
fn main() {
pub fn init_vault(password: &str) -> Result<()>
}
Initializes a new vault. Generates a random 32-byte identity secret, derives a key from the password using Argon2id, and encrypts the identity with ChaCha20-Poly1305. Writes identity.enc and config.toml to the vault directory.
unlock_vault()
#![allow(unused)]
fn main() {
pub fn unlock_vault(password: &str) -> Result<[u8; 32]>
}
Unlocks the vault by decrypting the identity secret. Returns the 32-byte master key used for file encryption. Fails if the password is incorrect (authentication tag mismatch).
export_vault()
#![allow(unused)]
fn main() {
pub fn export_vault(path: &Path, password: &str) -> Result<()>
}
Exports the vault as a single encrypted bundle file. The bundle format:
DOTLVAUL (8-byte magic)
0x01 (1 byte: version)
<32-byte salt> (Argon2id salt)
<12-byte nonce> (ChaCha20-Poly1305 nonce)
<ciphertext + tag> (encrypted payload)
The encrypted payload contains the vault config and identity secret.
import_vault()
#![allow(unused)]
fn main() {
pub fn import_vault(path: &Path, password: &str) -> Result<()>
}
Imports a vault from an encrypted bundle. Decrypts the bundle using the password, extracts the config and identity, and writes them to the vault directory.
change_password()
#![allow(unused)]
fn main() {
pub fn change_password(old_password: &str, new_password: &str) -> Result<()>
}
Changes the vault password. Decrypts the identity with the old password and re-encrypts with the new password.
sync
Entry deployment, fingerprint tracking, lifecycle hooks, and three-way merge.
sync::deploy
EntryState
#![allow(unused)]
fn main() {
pub enum EntryState {
Deployed,
Modified,
Missing,
Broken,
Conflict,
}
}
The current state of a tracked entry.
| State | Meaning |
|---|---|
Deployed | Entry is correctly deployed and in sync |
Modified | Entry has local changes that differ from the repo |
Missing | Target file is missing |
Broken | Symlink points to a non-existent target |
Conflict | Both repo and local file have changed |
check_state()
#![allow(unused)]
fn main() {
pub fn check_state(
entry: &Entry,
repo_root: &Path,
default_method: DeployMethod,
) -> EntryState
}
Checks the current deployment state of an entry.
deploy_entry()
#![allow(unused)]
fn main() {
pub fn deploy_entry(
entry: &Entry,
repo_root: &Path,
default_method: DeployMethod,
force: bool,
) -> Result<()>
}
Deploys an entry: creates a symlink or copies the file from the repo to the target path. When force is false, refuses to overwrite unmanaged files.
deploy_encrypted()
#![allow(unused)]
fn main() {
pub fn deploy_encrypted(
entry: &Entry,
repo_root: &Path,
password: &str,
) -> Result<()>
}
Deploys an encrypted entry: decrypts the repo file and writes the plaintext to the target path.
sync::fingerprint
EntryFingerprint
#![allow(unused)]
fn main() {
pub struct EntryFingerprint {
pub enc_hash: String,
pub target_hash: String,
pub source_hash: String,
}
}
Content hashes for an entry at the time of last sync.
| Field | Description |
|---|---|
enc_hash | Blake2s-256 hash of the encrypted file (if applicable) |
target_hash | Blake2s-256 hash of the deployed target file |
source_hash | Blake2s-256 hash of the repo source file |
WhichSide
#![allow(unused)]
fn main() {
pub enum WhichSide {
Unknown,
Neither,
RepoOnly,
ActualOnly,
Both,
}
}
Indicates which side of a sync pair has changed.
| Variant | Meaning |
|---|---|
Unknown | No fingerprint record exists |
Neither | Both sides match their fingerprints |
RepoOnly | Only the repo source has changed |
ActualOnly | Only the deployed target has changed |
Both | Both sides have changed (conflict) |
FingerprintStore
#![allow(unused)]
fn main() {
pub struct FingerprintStore { /* private */ }
}
Store for entry fingerprints, backed by ~/.dotling/fingerprints.toml.
FingerprintStore::load()
#![allow(unused)]
fn main() {
pub fn load(path: PathBuf) -> Self
}
Loads the fingerprint store from the given path.
FingerprintStore::has_record()
#![allow(unused)]
fn main() {
pub fn has_record(&self, source: &str) -> bool
}
Returns true if a fingerprint exists for the given source.
FingerprintStore::record()
#![allow(unused)]
fn main() {
pub fn record(
&mut self,
source: &str,
enc_path: &Path,
target_path: &Path,
) -> Result<()>
}
Records fingerprints for an encrypted entry.
FingerprintStore::record_plain()
#![allow(unused)]
fn main() {
pub fn record_plain(
&mut self,
source: &str,
source_path: &Path,
target_path: &Path,
) -> Result<()>
}
Records fingerprints for a plain (non-encrypted) entry.
FingerprintStore::is_in_sync()
#![allow(unused)]
fn main() {
pub fn is_in_sync(
&self,
source: &str,
enc_path: &Path,
target_path: &Path,
) -> Option<bool>
}
Returns Some(true) if the entry is in sync, Some(false) if not, None if no record exists.
FingerprintStore::who_changed()
#![allow(unused)]
fn main() {
pub fn who_changed(
&self,
source: &str,
source_path: &Path,
target_path: &Path,
) -> WhichSide
}
Determines which side has changed since the last sync.
FingerprintStore::save()
#![allow(unused)]
fn main() {
pub fn save(&self) -> Result<()>
}
Saves the fingerprint store to disk.
hash_path()
#![allow(unused)]
fn main() {
pub fn hash_path(path: &Path) -> Result<String>
}
Computes a Blake2s-256 hash of a file or directory (recursive).
hash_file()
#![allow(unused)]
fn main() {
pub fn hash_file(path: &Path) -> Result<String>
}
Computes a Blake2s-256 hash of a single file.
sync::hooks
HookSession
#![allow(unused)]
fn main() {
pub struct HookSession { /* private */ }
}
Manages hook execution with trust verification.
HookSession::new()
#![allow(unused)]
fn main() {
pub fn new(allow_hooks: bool, no_hooks: bool) -> Self
}
Creates a new hook session. allow_hooks auto-trusts all hooks. no_hooks disables all hook execution.
HookSession::verify_and_allow()
#![allow(unused)]
fn main() {
pub fn verify_and_allow(
&mut self,
command: &str,
hook_type: &str,
no_interactive: bool,
) -> Result<bool>
}
Checks if a hook command is trusted. If not, prompts the user for verification. Returns true if the hook should run.
HookSession::run_hook()
#![allow(unused)]
fn main() {
pub fn run_hook(
&mut self,
command: &str,
hook_type: &str,
repo_root: &Path,
dry_run: bool,
no_interactive: bool,
entry: Option<&Entry>,
entry_action: Option<&str>,
) -> Result<()>
}
Executes a hook command. Verifies trust first, sets environment variables, and retries up to 3 times on failure.
sync::merge
MergeResult
#![allow(unused)]
fn main() {
pub struct MergeResult {
pub content: String,
pub has_conflicts: bool,
pub conflict_count: usize,
}
}
Result of a three-way merge.
three_way_merge()
#![allow(unused)]
fn main() {
pub fn three_way_merge(
base: &str,
ours: &str,
theirs: &str,
ours_label: &str,
theirs_label: &str,
) -> MergeResult
}
Performs a line-level three-way merge using LCS (Longest Common Subsequence) diff. Non-overlapping changes are auto-merged. Overlapping conflicts are marked with git-style conflict markers:
<<<<<<< repo
repo version content
=======
actual local content
>>>>>>> actual
Contributing
Contributions are welcome! See CONTRIBUTING.md for guidelines, development setup, and PR workflow.
Quick reference
All development commands use just. Inside a Nix environment, prefix with nix develop --command.
| Task | Command |
|---|---|
| Build | just build |
| Release build | just release |
| Run | just run -- <args> |
| Test | just test |
| Type check | just check |
| Lint (clippy) | just clippy |
| Format | just fmt |
| Format check | just fmt-check |
| Full local CI | just ci |
Toolchain
- Rust nightly (pinned in
rust-toolchain.toml) - Edition 2024, MSRV 1.85
- Nix dev shell via
flake.nixwith direnv integration
Code style
- 100-char width, 4-space indent
- Imports grouped as
std -> external -> crate, sorted alphabetically - Trailing commas on multiline
- Run
just fmtbefore committing
Linting
Clippy warnings are treated as errors (-D warnings). Key constraints:
- Cognitive complexity threshold: 20
- Function line limit: 80
- Function arg limit: 6
- Banned:
std::thread::sleep,std::process::exit,std::env::temp_dir,dbg!,todo!,unimplemented!
Changelog
All notable changes to dotling are documented here.
Each release follows Keep a Changelog conventions: Added, Changed, Fixed, Removed.
v0.8.0
Changed
- Vault export/import — single encrypted bundle format —
vault exportnow writes a single encrypted bundle file instead of copying raw vault files.vault importdecrypts the bundle with your password and verifies the identity before writing. Old directory-based bundles are no longer compatible; re-export from your source machine after upgrading. - Encryption/decryption refactored —
encryptanddecryptnow operate in-place on tracked entries, consolidating file handling into shared helpers. Directory encryption uses the same pipeline as single-file encryption. - Fingerprint tracking for template entries — Templates now participate in the fingerprint store, enabling
statusandsync --dry-runto detect template drift without decrypting.
Removed
- Backup system — Removed the
dotling backupcommand, the--backupflag onsync, and all automatic backup-before-overwrite behavior.
Added
- Comprehensive roundtrip tests for encryption and decryption, and a full template sync lifecycle test suite.
v0.7.0
Added
- Shell completions (
dotling completions <SHELL>) — Generate tab-completion scripts for bash, zsh, fish, elvish, and powershell.
v0.6.2
Changed
- Module restructuring — Reorganized the codebase into a layered architecture with
core/,config/, andsync/top-level modules.
Fixed
- Minor formatting cleanup in the sync command hook error message.
v0.6.1
Added
dotling edit <entry>— Open any tracked file in your editor without manually decrypting and re-encrypting.
Fixed
- Hook retry logic — Hooks that fail are now retried up to 3 times before the sync is aborted.
v0.6.0
Added
- Dotfile templating (
dotling add --template,dotling vars) — Render machine-specific values into your dotfiles automatically. - Template syntax with
{{ var.key }}, built-in variables, environment variables, pipe filters, and whitespace control. dotling varssubcommand with seven actions: list, set, get, unset, check, import, export.- Bootstrap prompt for missing variables on new machines.
v0.5.0
Added
- Lifecycle hooks — Run custom commands before/after sync, globally or per-entry, with trust verification.
- Line-level three-way merge — Interactive merge option for copy-mode files during conflict resolution.
- Sync fingerprints — Blake2s-256 content hash tracking for encrypted and copy-mode entries.
dotling removeimprovements — Now restores tracked files to their original paths.
v0.4.0
Added
- Bidirectional
sync— Replaces the olddeploycommand. Syncs changes in both directions. - Recursive directory encryption —
encryptanddecryptnow handle entire directories.
Changed
removealways restores the original file and deletes the repo source.
v0.3.1
Fixed
- Vault architecture now correctly uses the master secret via key encapsulation.
- Absolute paths and
~-relative paths are resolved correctly during config lookups.
v0.3.0
Changed
- Rewrote core modules, replaced the printer with a UI layer, and simplified the CLI command structure.
v0.2.1
Added
- Automatic pull-back — modified entries are pulled back during push.
v0.2.0
Added
- Age-based encryption with key generation support.
- Core dotfiles management CLI and project scaffolding.
v0.1.0
Initial release.