dotm
A dotfile manager with composable roles, Tera templates, host-specific overrides, and system-level package support.
dotm organizes config files into packages (directories mirroring your target directory structure), groups them into roles (e.g. "desktop", "dev", "gaming"), and assigns roles to hosts. Deployment creates symlinks for plain files and copies for overrides/templates, so your dotfiles repo stays the single source of truth.
Installation
Or to install from the latest source:
Quick Start
# Initialize a dotm project
&&
# Create the root config
# Create a package
# Create a role
# Create a host config
# Deploy (dry run first)
Core Concepts
Packages
A package is a directory under packages/ that mirrors the target directory structure (usually ~). Files inside are deployed to their corresponding locations.
packages/
├── shell/
│ ├── .bashrc
│ └── .bash_profile
└── editor/
└── .config/
└── nvim/
└── init.lua
Target paths support environment variable expansion ($HOME, $XDG_CONFIG_HOME, ~, etc.). Undefined variables produce an error at deploy time.
Packages are declared in dotm.toml:
[]
= "Editor configuration"
= ["shell"] # always pulled in
= ["theme"] # informational only
= "$XDG_CONFIG_HOME" # supports ~, $VAR, ${VAR}
= "copy" # "stage" (default) or "copy"
Deployment Strategies
Each package uses one of two deployment strategies:
- stage (default) — files are copied to a
.staged/directory, then symlinked from the target location. The dotfiles repo stays the source of truth and changes to the staged copy are detected as drift. - copy — files are copied directly to the target location. No symlink, no staging directory. Useful for system files or contexts where symlinks aren't appropriate.
Roles
A role groups packages together and can define variables for template rendering. Role configs live in roles/<name>.toml:
# roles/desktop.toml
= ["shell", "editor", "kde"]
[]
= "fancy"
= "3840x2160"
Hosts
A host config selects which roles to apply and can override variables. Host configs live in hosts/<hostname>.toml:
# hosts/workstation.toml
= "workstation"
= ["desktop", "gaming", "dev"]
[]
= "3840x2160"
= "amd"
Variable precedence: host vars > role vars (last role listed wins among roles).
Directory Structure
~/dotfiles/
├── dotm.toml # root config: package declarations
├── hosts/
│ ├── workstation.toml
│ └── dev-server.toml
├── roles/
│ ├── desktop.toml
│ ├── dev.toml
│ └── gaming.toml
└── packages/
├── shell/
│ ├── .bashrc # plain file → symlinked
│ ├── .bashrc##host.dev-server # host override → copied
│ └── .bashrc##role.dev # role override → copied
├── editor/
│ └── .config/nvim/
│ └── init.lua
└── kde/
└── .config/
├── rc.conf
└── rc.conf.tera # template → rendered & copied
File Overrides
Override files sit next to the base file with a ## suffix:
| Pattern | Priority | Description |
|---|---|---|
file##host.<hostname> |
1 (highest) | Used only on the named host |
file##role.<rolename> |
2 | Used when the role is active |
file.tera |
3 | Tera template, rendered with vars |
file |
4 (lowest) | Base file, symlinked |
- Override and template files are copied, not symlinked
- Only the highest-priority matching variant is deployed
- Non-matching overrides are ignored entirely
Templates
Files ending in .tera are rendered using Tera (a Jinja2-like template engine). Variables come from role and host configs:
# .config/app.conf.tera
resolution={{ display.resolution }}
{% if gpu.vendor == "amd" %}
driver=amdgpu
{% else %}
driver=modesetting
{% endif %}
The .tera extension is stripped from the deployed filename.
File Permissions & Ownership
Packages can control file permissions and ownership. This is particularly useful for system packages but works for any package.
Per-file permissions
[]
= "755"
= "700"
Per-package ownership defaults
[]
= "root"
= "root"
Per-file ownership overrides
[]
= "root:appgroup"
Preserving existing metadata
When you want dotm to manage file content but leave existing ownership or permissions untouched on specific files:
[]
= ["owner", "group"]
= ["mode"]
Resolution order
For each file, each metadata field (owner, group, mode) is resolved independently:
- Per-file
preserve— keep existing value on disk - Per-file
ownership/permissions— explicit override - Package-level
owner/group— default for all files in the package - Nothing configured — preserve existing value on disk
The default behavior is to preserve. Setting metadata is always opt-in.
Hooks
Packages can define shell commands to run before and after deploy/undeploy operations:
[]
= "Shell configuration"
= "echo 'deploying shell configs...'"
= "source ~/.bashrc"
= "echo 'removing shell configs...'"
= ""
- Commands run via
sh -cwith the package's target directory as the working directory - Environment variables
DOTM_PACKAGE,DOTM_TARGET, andDOTM_ACTIONare set pre_*hook failure aborts the operation for that package;post_*failures are warnings- Hooks are skipped during
--dry-run
Orphan Detection
When files are removed from a package or a package is removed from a role, previously deployed files become "orphans." dotm detects these on deploy and warns about them:
)
To automatically remove orphans on every deploy:
[]
= "~"
= true
Or run dotm prune manually to clean up.
System Packages
dotm can deploy configuration files to system locations like /etc/. System packages are deployed separately from user packages, under root privileges.
Configuration
Mark a package as system-level with system = true. System packages must explicitly set target and strategy:
[]
= "NetworkManager configs"
= true
= "/etc/NetworkManager"
= "copy"
= "root"
= "root"
[]
= "root:networkmanager"
[]
= "640"
Usage
System packages are deployed separately from user packages using the --system flag:
# Deploy user packages (system packages are skipped)
# Deploy system packages (requires root)
# Check system package status
# Restore system files to pre-dotm state
State separation
User and system packages maintain separate state:
| Context | State directory | Staging directory |
|---|---|---|
| User | ~/.local/state/dotm/ |
<dotfiles>/.staged/ |
| System | /var/lib/dotm/ |
/var/lib/dotm/.staged/ |
Drift Detection
dotm tracks the content hash and metadata of every deployed file. When files are modified externally, dotm detects the drift:
Status markers:
| Marker | Meaning |
|---|---|
~ |
File is OK (verbose mode only) |
M |
Content has been modified since last deploy |
! |
File is missing |
P |
File metadata (owner/group/permissions) has drifted |
If a file was modified externally, re-deploying will skip it with a warning. Use --force to overwrite, or dotm adopt to pull the changes back into your dotfiles repo.
CLI Reference
dotm [OPTIONS] <COMMAND>
Options:
-d, --dir <DIR> Path to dotfiles directory [default: .]
-V, --version Print version
Commands:
deploy Deploy configs for the current host
undeploy Remove all managed symlinks and copies
restore Restore files to their pre-dotm state
status Show deployment status
diff Show diffs for files modified since last deploy
adopt Interactively adopt changes back into source
check Validate configuration
init Initialize a new package
add Add existing files to a package
list List available packages, roles, or hosts
prune Remove orphaned files no longer managed by any package
completions Generate shell completions
commit Commit all changes in the dotfiles repo
push Push dotfiles repo to remote
pull Pull dotfiles repo from remote
sync Pull, deploy, and optionally push in one step
deploy
undeploy
restore
restore differs from undeploy: if dotm overwrote an existing file, restore puts the original back. undeploy just removes the file.
status
diff
adopt
check
Validates package dependencies, host/role references, system package requirements (target and strategy must be set), ownership format, permission values, and preserve/override conflicts.
init
add
Moves existing files into a package directory and prints a summary. Run dotm deploy afterward to create symlinks back to the original locations.
list
prune
completions
commit / push / pull / sync
Comparison
| Feature | dotm | GNU stow | yadm | dotter |
|---|---|---|---|---|
| Symlink-based | Yes | Yes | Yes | Yes |
| Role/profile system | Yes | No | No | Yes |
| Host-specific overrides | Yes | No | Alt files | Yes |
| Template rendering | Tera | No | Jinja2* | Handlebars |
| Dependency resolution | Yes | No | No | No |
| Per-package target dirs | Yes | Yes | No | No |
| System file deployment | Yes | No | No | No |
| File ownership control | Yes | No | No | No |
| Drift detection | Yes | No | No | Yes |
| Pre-existing file backup | Yes | No | No | No |
*yadm templates require a separate yadm alt step.
Disclaimer
Claude Code (Opus 4.6) was used for parts of the development of this tool, including some implementation, testing and documentation.
License
GNU AGPLv3