etcdotica: dotfiles and system config management
etcdotica is a lightweight, file-based overlay that synchronizes system configuration with a Git repository. It treats the repository as a source of truth that casts a shadow onto the filesystem: only tracked paths are managed, while everything else remains undisturbed.
This approach provides a predictable, reversible way to manage dotfiles and system artifacts without heavy abstractions or an intermediate configuration layer.
Architecture overview
Per-file overlay model
The tool operates with file-level granularity, treating your repository as a partial map of the system:
- Any file in your source repository is synchronized to its corresponding system path. Files are copied only if content (size or modification time) or permissions have changed.
- Files that exist on the system but are absent from the repository are ignored.
etcdoticadoes not own your directories; it only manages the specific artifacts you explicitly track. - Only files previously managed by
etcdoticaand later deleted from the source are removed from the destination. This is tracked via a local.etcdoticastate file.
Bidirectional workflow
Configuration often happens directly on the system. etcdotica supports a circular workflow:
- Push changes from your repository to the system.
- Pull local tweaks from the system back into your repository.
Watch mode and safe concurrency
The tool can monitor the source directory for changes and apply them instantly when files are saved. If enabled, it can also collect system-side changes automatically.
It uses advisory file locking (flock) to ensure multiple instances can run safely without corrupting files or state. For example, you can run one instance in watch mode while another runs as part of a periodic deployment script.
Initial provisioning
For fresh installations, the tool can prioritize repository files over existing system defaults. This reduces machine provisioning to a simple process: clone your repository and run a single command to align the system with your saved state.
Section-based file management
For large system files where only specific lines need to be managed, such as entries in /etc/fstab or /etc/hosts, etcdotica supports sections. This allows you to maintain unique configuration snippets without taking ownership of the entire system-generated file. Sections are updated as the source files change, and if a section file is deleted from the source, the corresponding section is automatically removed from the target file.
Flexible scope and privilege model
etcdotica does not assume any fixed layout or ownership model. You map source directories to destination paths directly and run it with whatever privileges the target requires. This makes the scope entirely opt-in: you can manage a full dotfiles tree or just a handful of files, and apply the same pattern to system configuration without taking ownership of unrelated parts of the filesystem.
It applies the provided or user-default umask to ensure files are copied with secure permissions. It can optionally enforce world readability.
It can also optionally ensure that all files within specified source directories (such as bin/) have executable bits set before syncing.
What's in a name?
etcdotica fuses the Unix /etc directory with the Italian term Ecdotica (ecdotics in English). This scholarly discipline is devoted to reconciling divergent manuscript witnesses to produce a "critical edition", a definitive version of a text reconstructed from centuries of manual copying.
The metaphor is deliberate. Curating a modern system is an editorial act. Most configurations are not authored once so much as they are transmitted: a tradition of inherited snippets from strangers and fragments of half-remembered internet threads that somehow survive a decade of migrations, reinstalls, and late-night edits.
etcdotica is a small attempt at editorial hygiene for that tradition. It allows you to maintain your configuration in plain text and apply it convergently, without the need for an intermediate software layer that generates and applies your actual configuration.
And despite how the name sounds, there is no distributed consensus here. It simply ensures that the transmission of your .bashrc across your personal digital history suffers fewer scribal errors.
High-level example
Assume your Git repository lives at ~/.dotfiles and has the following structure:
home/.bashrc
home/.config/systemd/user/etcdotica.service
root/etc/ssh/sshd_config.d/disable-password-authentication.conf # illustrative example
root-only/root/.bashrc
You apply the repository files to the system in three passes, each using the appropriate privilege level and scope:
cd ~/.dotfiles # 1. User-level files under `home/` etcdotica \ -src home \ -bindir .local/bin \ -umask 077 \ -collect # 2. Root-owned files that must never be readable by unprivileged users sudo etcdotica \ -src root-only \ -umask 077 \ -collect # 3. System-wide configuration from `root/`, ensuring files are readable for all users sudo etcdotica \ -src root \ -bindir usr/local/bin \ -everyone \ -collect
The -bindir option is a quality-of-life feature. Any file placed under the specified directory inside the repository is automatically marked executable when synced, so newly created helper scripts are immediately runnable without a manual chmod.
To keep user files continuously synchronized, define a user systemd service at ~/.config/systemd/user/etcdotica.service:
[Unit] Description=Etcdotica ConditionPathExists=%h/.dotfiles [Service] Type=exec WorkingDirectory=%h/.dotfiles ExecStart=/usr/local/bin/etcdotica \ -src home \ -bindir .local/bin \ -umask 077 \ -collect \ -watch [Install] WantedBy=default.target
Enable and start the service:
systemctl --user enable --now etcdotica.serviceWith this setup, editing any file in ~/.dotfiles/home is immediately reflected in your home directory, while still allowing changes made directly on the system to be collected back into the repository.
You can place the service unit file in your ~/.dotfiles repository at home/.config/systemd/user/etcdotica.service, but you must do this before the first manual sync as described above, or simply rerun the sync.
Installation
To install etcdotica, you need the Go toolchain and the just command runner configured on your system.
-
Clone the repository:
git clone https://github.com/senotrusov/etcdotica.git && cd etcdotica
-
Install the binary:
(This will compile the binary and perform a system-wide installation to
/usr/local/binusingsudo)
Building for development
To compile the executable for local testing without installing it to the system:
(The resulting binary will be placed in the ./bin/ directory)
Usage
To use etcdotica, run the binary. You must specify the source directory using the -src flag.
You can optionally specify the destination using the -dest flag; by default, it uses the user's home directory, or / when running as root.
It automatically excludes .git directories and its own state file from synchronization.
Options
| Flag | Type | Description |
|---|---|---|
-bindir |
string |
Directory relative to the source directory in which all files will be ensured to have the executable bit set (can be repeated). |
-collect |
bool |
Collect mode: copy newer files from destination back to source. Ignored if -force is enabled. |
-dst |
string |
Destination directory (default: user home directory, or / if root). |
-everyone |
bool |
Set group and other permissions to the same permission bits as the owner, then apply the umask to the resulting mode. |
-force |
bool |
Force overwrite even if destination is newer. Overrides -collect. |
-help |
bool |
Show help and usage information. |
‑log‑format |
string |
Log format: human, text or json (default "human"). |
‑log‑level |
string |
Log level: debug, info, warn, error (default "info"). |
-src |
string |
Source directory (required). |
-umask |
string |
Set process umask (octal, e.g. 077). |
-version |
bool |
Print version information and exit. |
-watch |
bool |
Watch mode: scan continuously for changes. |
Environment variables
etcdotica also respects the following environment variables, which can be useful for containerized environments or scripts:
| Variable | Description |
|---|---|
EDTC_LOG_LEVEL |
Sets the default log level (debug, info, warn, error). Overridden by -log-level. |
EDTC_FORCE |
If set to 1 or true, enables force mode (equivalent to -force). Overrides collect mode. |
EDTC_COLLECT |
If set to 1 or true, enables collect mode (equivalent to -collect). Ignored if force mode is enabled. |
Examples
-
Apply changes from your dotfiles folder to your Home directory:
etcdotica -src ~/my-dotfiles -
Keep the program running to verify changes live, outputting logs in JSON for processing:
etcdotica -src ~/my-dotfiles -watch -log-format json -
Ensure all files in
bin/andscripts/have executable permissions set before syncing:etcdotica -src ~/my-dotfiles -bindir .local/bin -bindir scripts -
If you edited a config file directly in your home directory and want to save it back to your repo:
etcdotica -src ~/my-dotfiles -collect -
Sync configurations to
/etcensuring they are readable by all users:sudo etcdotica -src ./etc-files -dst /etc -everyone
State & pruning
etcdotica creates a hidden file named .etcdotica in your source directory. This file tracks every file and section successfully synced.
- If you delete a file from your source directory,
etcdoticadetects its absence compared to the state file and removes the corresponding file from the destination. - If you delete a section file (e.g.,
etc/fstab.external-disks-section) from the source,etcdoticawill automatically find the target file (etc/fstab) and remove only the block belonging to that specific section, leaving the rest of the file untouched. - If running as root (e.g., via
sudo),etcdoticaattempts to set the ownership of the.etcdoticastate file to match the owner of the source directory. This prevents the state file from becoming locked to root, ensuring you can still modify your dotfiles repository as a standard user later.
Managed sections
etcdotica supports a special "section" mode that allows you to manage parts of a file without owning the entire file. This is useful for shared system files like /etc/fstab or /etc/hosts.
Naming convention
To use this feature, name your source file using the pattern: filename.{section-name}-section.
Example
- Source:
etc/fstab.external-disks-section - Target:
etc/fstab - Section Name:
external-disks
How it works
The content of the source file is wrapped in # BEGIN and # END markers and inserted into the target file.
- If multiple sections exist in the target file,
etcdoticasorts them alphabetically by their section name. - If a section with the same name already exists, its content is replaced.
- If no sections exist, the new section is appended to the end of the file.
- If other sections exist, the new section is inserted in its correct alphabetical position relative to other blocks.
All text outside of # BEGIN / # END blocks is preserved exactly as it is.
Safety and validation
To prevent data loss or corruption, etcdotica performs safety checks on the destination file:
- If the target file contains a
# BEGINor# ENDtag that matches your section name but is missing its counterpart (e.g., a start tag with no end tag),etcdoticawill stop and refuse to modify the file. - Malformed tags for sections with different names are ignored and treated as raw text to avoid interference with existing file content.
Symlink behavior at destination
To ensure safety and predictability, etcdotica follows specific rules when it encounters an existing symlink at the destination path:
- If the source is a regular file but the destination is a symlink,
etcdoticawill remove the symlink and replace it with a standard file. This is a safety feature: it prevents the tool from accidentally overwriting the contents of a file located elsewhere on your system that the symlink might be pointing to. - If a destination path is a symlink that points to an existing directory,
etcdoticawill preserve the symlink and sync the source contents into the directory it points to. This allows you to transparently redirect entire configuration folders (such as symlinking~/.config/appto a different drive) while still allowingetcdoticato manage the files inside.
When the tool identifies an orphaned file at the destination that needs to be removed (because it no longer exists in the source), it uses a safe removal method. If that orphaned file is a symlink, only the symlink pointer itself is deleted; the file or directory it was pointing to remains untouched.
Concurrency & safety
etcdotica is designed for robust operation. It uses advisory file locking (flock) on the destination files, section-managed files, and its own .etcdotica state file.
This means:
- You can leave one instance running in
-watchmode and manually trigger another sync without risk of data corruption or race conditions. - Multiple users or scripts can safely run
etcdoticaagainst the same destination or source simultaneously. - While it writes directly to files (to preserve Inodes and hardlinks), the exclusive lock ensures that no other process using standard locking will read a partially written file.
Direct writes & inode stability
etcdotica writes directly to destination files instead of using a "write-to-temp and rename" strategy. This design prioritizes three factors:
-
Writing in place keeps the file's inode constant. This preserves existing hardlinks and ensures active system watches (such as
inotify) remain attached to the file. -
Performing a truly atomic rename requires placing the temporary file on the same filesystem as the destination. Reliably identifying a safe, writable location for these transient files across varying mount points adds significant complexity that falls outside the scope of this tool for now. Choosing to create the temporary file in the same directory as the destination can introduce race conditions, because many directories are automatically scanned for configuration files and not all services reliably ignore temporary files.
-
While an atomic write per file sounds appealing, many services load multiple configuration files, so they are still updated one by one, leaving intermediate states observable regardless of per-file atomicity.
-
Most services, particularly under
/etc, traditionally require an explicit reload or restart command to apply configuration changes, so direct writes are generally acceptable because changes are not picked up until the service is reloaded. -
In practice, a transient write failure can usually be resolved by simply re-running the command, which overwrites any partial writes and converges the system to the intended state.
This approach introduces a millisecond-wide window where a service might attempt to read a partially written file if that service does not respect file locks. This is a deliberate choice: in system configuration, a temporary partial read is generally safer and more predictable than the logic conflicts caused by "seeing" extra files in a managed directory.
Resilience & fault tolerance
If a source directory becomes unavailable during Watch Mode, possibly due to user actions or temporary network unavailability for remote drives, etcdotica logs a warning and waits for the source to reappear. Synchronization then resumes automatically, provided the source was successfully located at least once during startup.
License
etcdotica is dual-licensed under the Apache License, Version 2.0 and the MIT License. You can choose to use it under the terms of either license. By contributing, you agree to license your contributions under both licenses.