Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 syncdotling sync pushes from repo to actual and pulls from actual to repo automatically
  • Automatic path mapping~/.config/nvim becomes config/nvim, ~/.zshrc becomes shell/zshrc
  • Multi-OS support — tag entries as linux, macos, or windows; 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 git commands
  • Dotfile templating — add machine-specific values using {{ var.key }} syntax; render on every sync
  • Health checksdotling doctor audits 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

Installation

cargo install dotling

Prebuilt binaries

Download a prebuilt binary from the latest GitHub release:

PlatformBinary
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

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.

FieldTypeDefaultDescription
methodstring"symlink"Default deployment method: "symlink" or "copy"

[hooks]

Global lifecycle hooks that run at the beginning and end of dotling sync.

FieldDescription
initCommand run during dotling init (also runs when adopting or cloning)
beforeCommand run before any entries are synced
afterCommand 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

FieldTypeDescription
sourcestringRepo-relative path (required)
targetstringDeploy target path with ~ support (required)
methodstringOverride: "symlink" or "copy"
encryptedbooleantrue, 1, or yes — marks entry as encrypted
directorybooleantrue, 1, or yes — marks entry as a directory
templatebooleantrue, 1, or yes — marks entry as a template
osstring"all" (default), "linux", "macos", or "windows"
permissionsstringOctal permissions applied on sync, e.g. "0600"
beforestringEntry-level hook run before sync
afterstringEntry-level hook run after sync

Note: Template entries are marked with template: true in dotling.toml (set automatically by dotling add --template).

Path Mapping

Files are organized into categories automatically when added:

Home pathRepo path
~/.config/nvim/init.luaconfig/nvim/init.lua
~/.zshrcshell/zshrc
~/.bashrcshell/bashrc
~/.gitconfiggit/gitconfig
~/.vimrcvim/vimrc
~/.tmux.conftmux/tmux.conf
~/.ssh/configssh/config
~/.gnupg/gpg.confgnupg/gpg.conf
~/.somerchome/somerc

The mapping rules are:

  • .config/* files go to config/
  • Shell files (.zshrc, .bashrc, .bash_profile, .profile, .zprofile, .zshenv, .fishrc) go to shell/
  • Git files (.gitconfig, .gitignore_global) go to git/
  • Vim files (.vimrc, .gvimrc) go to vim/
  • Tmux files (.tmux.conf) go to tmux/
  • SSH files (.ssh/) go to ssh/
  • GnuPG files (.gnupg/) go to gnupg/
  • 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

ExpressionDescription
{{ 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 -c on Unix and cmd /C on 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:

  1. Local store~/.dotling/vars.toml (machine-specific, never committed)
  2. Config defaults[vars] in dotling.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:

FilePurpose
identity.encEncrypted secret (your vault’s master key)
config.tomlVault 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
DirectionTrigger
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 status or sync checks, dotling compares current file hashes against the stored fingerprint
  • Benefit: You can run dotling status or dotling sync --dry-run without 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 typePush (repo -> actual)Pull (actual -> repo)
SymlinkCreate/fix symlinkNever (symlink always reads repo)
CopySource newer or target missingTarget newer
EncryptedSource newer or target missing -> decryptTarget newer -> re-encrypt
TemplateAlways renders and deploysNever (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:

  1. Checks if the entry’s OS matches the current platform (skips if not)
  2. Determines the entry state: Deployed, Modified, Missing, Broken, or Conflict
  3. Uses fingerprint comparison (for copy/encrypted) or mtime to decide direction
  4. If both sides changed, prompts for conflict resolution (unless --force or --no-interactive)
  5. Executes the sync action (push or pull)
  6. Records fingerprints for future comparison
  7. 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:

OptionKeyDescription
Keep LocalkOverwrite the repo with your local file (pulls to repo)
Use ReporOverwrite the local file with the repo version (pushes to local)
MergemPerform a three-way merge (see below)
DiffdCompare inline changes
SkipsLeave 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 status and dotling sync --dry-run work 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:

HookWhen it runs
initDuring dotling init (also runs when adopting or cloning)
beforeBefore any entries are synced
afterAfter all entries are successfully synced

Entry-level hooks

Each entry can define its own hooks:

HookWhen it runs
beforeBefore this entry is pushed or pulled
afterAfter this entry is successfully pushed or pulled

Execution context

Hooks are executed in the repository root directory. The following environment variables are set:

VariableDescription
DOTLING_HOOK_TYPEglobal_init, global_before, global_after, entry_before, entry_after
DOTLING_REPO_ROOTAbsolute 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 set DOTLING_ALLOW_HOOKS=1) to auto-execute all hooks
  • Pass --no-hooks (or set DOTLING_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

FlagDescription
--dry-runShow what would change without modifying anything
--forceOverwrite conflicting files without prompting (repo wins)
--prefer-actualWhen both sides differ, prefer the local file (alias: --prefer-local)
--no-interactiveSkip conflicting entries and print a warning
--allow-hooksExecute all hooks without prompting
--no-hooksDisable 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

CommandDescription
dotling initInitialize a new repo or clone an existing one
dotling addMove files into the repo and deploy symlinks/copies
dotling removeUntrack entries and restore files to original locations
dotling syncBidirectional sync between repo and actual filesystem
dotling statusShow deployment status of all tracked entries
dotling editEdit a tracked entry in $EDITOR
dotling encryptEncrypt tracked entries
dotling decryptDecrypt encrypted entries
dotling vaultManage the encryption vault
dotling doctorAudit repository health
dotling varsManage machine-local template variables
dotling completionsGenerate shell completion scripts

Global flags

FlagDescription
-v, --verboseShow hints and additional details
-h, --helpPrint help
-V, --versionPrint version

Key command flags

CommandFlagDescription
add--copyDeploy as a copy instead of a symlink
add--encryptEncrypt the file(s) using the vault password
add--templateTrack as a template: rendered on each sync
add--os <platform>Target OS: linux, macos, windows
sync--dry-runPreview changes without modifying anything
sync--forceOverwrite conflicting files (repo wins)
sync--prefer-actualPrefer the local file on conflict
sync--no-interactiveSkip conflicts, print warnings
sync--allow-hooksExecute all hooks without prompting
sync--no-hooksDisable all hook execution
status--diffShow 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, runs git init, and registers the repo root
  • A git URL — clones the repo to ~/dotfiles, registers the repo root, and suggests running dotling sync to deploy entries

After initialization, the repo root is stored in ~/.dotling/state.toml so dotling knows where to find it.

What happens

  1. Creates the repo directory (or clones from URL)
  2. Writes a default dotling.toml with empty sections
  3. Runs git init (for new repos)
  4. Registers the repo root in ~/.dotling/state.toml
  5. Runs the [hooks] init command if defined
  6. For cloned repos: suggests running dotling sync to 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

ArgumentDescription
<PATHS>One or more file or directory paths to track

Options

FlagDescription
--encryptEncrypt the file(s) using the vault password
--copyDeploy as a copy instead of a symlink
--templateTrack 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

ArgumentDescription
<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:

  1. Undeploy — removes the symlink (or copy) from the target path
  2. Restore — moves the file from the repo back to its original target location
  3. Decrypt — if the entry was encrypted, decrypts the file before restoring
  4. Clean up — removes empty parent directories in the repo
  5. 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

FlagDescription
--dry-runShow what would change without modifying anything
--forceOverwrite conflicting files without prompting (repo wins)
--prefer-actualWhen both sides differ, prefer the local file (alias: --prefer-local)
--no-interactiveSkip conflicting entries and print a warning
--allow-hooksExecute all hooks without prompting
--no-hooksDisable 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

FlagDescription
--diffShow 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

StatusMeaning
DeployedEntry is correctly deployed and in sync
ModifiedEntry has local changes that differ from the repo
MissingTarget file is missing
BrokenSymlink points to a non-existent target
ConflictBoth repo and local file have changed

Sync badges

BadgeMeaning
[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

ArgumentDescription
<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:

  1. Decrypts the file to a secure temp file in ~/.dotling/tmp/ (mode 0600)
  2. Opens the temp file in your editor
  3. Re-encrypts the content and writes it back to the repo
  4. Securely wipes (overwrites with zeros) and deletes the temp file

Editor resolution

The editor is resolved in this order:

  1. $DOTLING_EDITOR — highest priority
  2. $VISUAL — standard Unix convention
  3. $EDITOR — standard fallback
  4. vim -> 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

ArgumentDescription
<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

ArgumentDescription
<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

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.toml parses 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

  1. Local store~/.dotling/vars.toml (machine-specific, never committed)
  2. Config defaults[vars] in dotling.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

ArgumentDescription
<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

VariableDescription
NO_COLORDisables ANSI color output when set (follows the no-color.org standard)
DOTLING_EDITORHighest-priority editor override for dotling edit
VISUALEditor override (standard Unix convention)
EDITOREditor fallback
DOTLING_ALLOW_HOOKSSet to 1 or true to auto-trust and execute all hooks without prompting
DOTLING_NO_HOOKSSet to 1 or true to completely disable hook execution
HOSTNAMEFallback hostname for dotling.hostname built-in if the syscall fails (Unix/Linux/macOS)
COMPUTERNAMEFallback hostname for dotling.hostname built-in on Windows
USER / USERNAMEUsed for dotling.username built-in (USERNAME is the Windows fallback)

Hook context variables

These variables are set in the environment when hooks execute:

VariableDescription
DOTLING_HOOK_TYPEType of hook: global_init, global_before, global_after, entry_before, entry_after
DOTLING_REPO_ROOTAbsolute 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/directories
  • Hooks — global lifecycle hooks
  • Vec<(String, String)> — shared variable defaults

The TOML parser and serializer are hand-rolled (no serde dependency).

Entry

Each tracked file or directory:

FieldTypeDescription
sourceStringRepo-relative path
targetStringDeploy target path with ~
methodOption<DeployMethod>Symlink or Copy override
encryptedboolWhether the entry is encrypted
directoryboolWhether the entry is a directory
templateboolWhether the entry is a template
osOption<String>Platform restriction
permissionsOption<u32>Octal permissions
beforeOption<String>Pre-sync hook
afterOption<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

CratePurpose
clapCLI argument parsing with derive macros
clap_completeShell completion generation
chacha20poly1305AEAD encryption
argon2Password-based key derivation
blake2Content hashing (fingerprints, hook trust)
randCryptographic random number generation
base64Binary-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

ModuleDescription
coreError types, filesystem helpers, path mapping, platform detection, state store
configConfiguration data model, TOML parser, template engine, variable store
cryptoChaCha20-Poly1305 encryption, Argon2id key derivation, vault management
syncEntry 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.

VariantUsage
IoFilesystem errors with path and operation context
ConfigConfiguration parsing errors with optional line number
CryptoEncryption/decryption failures
DeployDeployment errors with entry name
VaultVault operation failures
TemplateTemplate rendering errors with source file
UserUser-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.

#![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.

#![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.

#![allow(unused)]
fn main() {
pub fn is_symlink(path: &Path) -> bool
}

Returns true if the path is a symbolic 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:

PatternRepo 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 elsehome/

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.

FieldDescription
sourceRepo-relative path
targetDeploy target path with ~ support
methodOverride for the default deployment method
encryptedWhether the entry is encrypted
directoryWhether the entry is a directory
templateWhether the entry is a template
osPlatform restriction ("linux", "macos", "windows", or None for all)
permissionsOctal permissions applied on sync
beforeEntry-level pre-sync hook
afterEntry-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.

FieldDescription
rawThe full expression (e.g., "var.hostname")
namespaceThe namespace: "dotling", "var", or "env"
keyThe 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.

StateMeaning
DeployedEntry is correctly deployed and in sync
ModifiedEntry has local changes that differ from the repo
MissingTarget file is missing
BrokenSymlink points to a non-existent target
ConflictBoth 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.

FieldDescription
enc_hashBlake2s-256 hash of the encrypted file (if applicable)
target_hashBlake2s-256 hash of the deployed target file
source_hashBlake2s-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.

VariantMeaning
UnknownNo fingerprint record exists
NeitherBoth sides match their fingerprints
RepoOnlyOnly the repo source has changed
ActualOnlyOnly the deployed target has changed
BothBoth 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.

TaskCommand
Buildjust build
Release buildjust release
Runjust run -- <args>
Testjust test
Type checkjust check
Lint (clippy)just clippy
Formatjust fmt
Format checkjust fmt-check
Full local CIjust ci

Toolchain

  • Rust nightly (pinned in rust-toolchain.toml)
  • Edition 2024, MSRV 1.85
  • Nix dev shell via flake.nix with direnv integration

Code style

  • 100-char width, 4-space indent
  • Imports grouped as std -> external -> crate, sorted alphabetically
  • Trailing commas on multiline
  • Run just fmt before 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 formatvault export now writes a single encrypted bundle file instead of copying raw vault files. vault import decrypts 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 refactoredencrypt and decrypt now 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 status and sync --dry-run to detect template drift without decrypting.

Removed

  • Backup system — Removed the dotling backup command, the --backup flag on sync, 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/, and sync/ 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 vars subcommand 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 remove improvements — Now restores tracked files to their original paths.

v0.4.0

Added

  • Bidirectional sync — Replaces the old deploy command. Syncs changes in both directions.
  • Recursive directory encryptionencrypt and decrypt now handle entire directories.

Changed

  • remove always 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.