A convention-driven CLI wrapper around GNU Stow for managing dotfiles across multiple machines with layered overrides.
Managing dotfiles across machines with different OSes, hardware profiles, and Linux distros means lots of conditional logic in install scripts. Dotlayer replaces that with a naming convention: directory names declare when they should be stowed.
config/ → always stowed
config-linux/ → stowed on Linux
config-macos/ → stowed on macOS
config-omarchy/ → stowed when Omarchy is detected
config-omarchy-desktop/ → stowed on Omarchy + desktop profile
config-omarchy-laptop/ → stowed on Omarchy + laptop profile
config-mycompany/ → stowed when mycompany group is detected
No config file needed — just name your directories and dotlayer figures out the rest.
gem install dotlayerOr clone and run directly:
git clone https://github.com/douglas/dotlayer.git
export PATH="$HOME/src/dotlayer/exe:$PATH"Requirements: Ruby >= 3.2, GNU Stow
# See what dotlayer detects on your system
dotlayer status
# Stow all matching packages
dotlayer install
# Pull latest changes and re-stow
dotlayer update
# Check for broken symlinks and missing deps
dotlayer doctor
# Move existing config into a stow package
dotlayer adopt ~/.config/lazygit config
# Move config into the private repo
dotlayer adopt --private ~/.config/lazysql configDotlayer auto-detects four things about your system:
| Detection | Method | Example |
|---|---|---|
| OS | RbConfig::CONFIG["host_os"] |
linux, macos |
| Profile | hostnamectl chassis or $DOTLAYER_PROFILE |
desktop, laptop |
| Distro | Shell commands from config | omarchy, fedora |
| Group | Shell commands from config | work, mycompany |
It then scans your dotfiles repo for directories matching these tags and stows them in layer order:
- Base packages —
stow,bin,git,zsh,config - OS layer —
config-linuxorconfig-macos - Distro layer —
config-omarchy - Distro + profile —
config-omarchy-desktop - Group layer —
config-mycompany
Each layer can add files but never conflict with earlier layers (Stow constraint).
Dotlayer works with zero configuration using sensible defaults. For customization, create a dotlayer.yml:
target: ~
repos:
- path: ~/.public_dotfiles
- path: ~/.private_dotfiles
private: true
packages:
- stow
- bin
- git
- zsh
- config
profiles:
detect: hostnamectl chassis
env: DOTLAYER_PROFILE
distros:
omarchy:
detect: test -d ~/.local/share/omarchy
fedora:
detect: . /etc/os-release && test "$ID" = "fedora"
groups:
mycompany:
detect: test -d ~/src/mycompany
system_files:
- source: config-linux/etc/systemd/system-sleep/xremap-restart.sh
dest: /etc/systemd/system-sleep/xremap-restart.sh
mode: "0755"
hooks:
after_system_files:
- sudo udevadm control --reload-rulesConfig file is discovered automatically from:
~/.config/dotlayer/dotlayer.yml~/.public_dotfiles/dotlayer.yml~/.dotfiles/dotlayer.yml
Most people have two kinds of config: stuff you can share (shell aliases, editor themes, window manager rules) and stuff you can't (API keys, work credentials, employer-specific tooling). Dotlayer handles this with two repos:
~/.public_dotfiles/ ← shared on GitHub, safe for the world to see
~/.private_dotfiles/ ← private repo (or not pushed at all)
A single dotfiles repo forces a choice: keep it private (lose the benefit of sharing) or keep it public (risk leaking secrets). Two repos solves this cleanly:
- Public repo — your shell, editor, git, and desktop config. Push to GitHub, share with the community, clone on any new machine.
- Private repo — work-specific config, API tokens, SSH configs, fonts with restrictive licenses, employer tooling. Keep in a private repo or don't push at all.
Both repos stow into the same target (~), so your home directory looks the same regardless of which repo a file came from.
Create both repos:
mkdir -p ~/.public_dotfiles ~/.private_dotfiles
cd ~/.public_dotfiles && git init
cd ~/.private_dotfiles && git initAdd the dotlayer config at ~/.config/dotlayer/dotlayer.yml:
repos:
- path: ~/.public_dotfiles
- path: ~/.private_dotfiles
private: true
packages:
- config
- fontsThe private: true flag tells dotlayer which repo to use when you run dotlayer adopt --private.
Public repo — uses the standard base packages (stow, bin, git, zsh, config) with full layering support:
~/.public_dotfiles/
config/ ← shared config (editor, terminal, etc.)
config-linux/ ← Linux-specific overrides
config-omarchy/ ← Omarchy distro overrides
config-omarchy-desktop/ ← Omarchy + desktop profile
git/ ← git config
zsh/ ← shell config
bin/ ← personal scripts
stow/ ← stow's own config
Private repo — can use per-repo packages for layered resolution, plus standalone directories that are all stowed automatically:
~/.private_dotfiles/
config/ ← private config overlays (SSH, API keys)
config-mycompany/ ← work-specific config (group layer)
fonts/ ← licensed fonts
fonts-linux/ ← Linux-specific font config
claude/ ← standalone: Claude AI config
work/ ← standalone: work tooling
scripts/ ← standalone: work automation scripts
Directories that match the per-repo base packages (config, fonts) get full layer resolution. Everything else (claude, work, scripts) is stowed as-is in alphabetical order.
# Public config — goes to ~/.public_dotfiles
dotlayer adopt ~/.config/lazygit config
# Private config — goes to ~/.private_dotfiles
dotlayer adopt --private ~/.config/lazysql configRepos are processed in order. Public packages are stowed first, then private packages overlay on top. This means:
~/.public_dotfiles/config/— base config (stowed first)~/.public_dotfiles/config-linux/— OS layer~/.public_dotfiles/config-omarchy/— distro layer~/.private_dotfiles/config/— private overlays (stowed after public)~/.private_dotfiles/config-mycompany/— work group layer
Each layer adds files — they never conflict because different layers use different file paths within the same directory structure.
Your private repo doesn't need to be pushed anywhere. It works fine as a local-only git repo for tracking changes. If you do want backup:
- Push to a private GitHub/GitLab repo
- Use
dotlayer updateto pull both repos at once — it runsgit pull --rebaseon each repo that has a.gitdirectory
dotlayer [options] <command>
Commands:
install Detect system, stow all layers, install system files
update Pull repos, re-stow all layers
status Show detected OS, profile, distros, groups, and packages
doctor Check for broken symlinks, missing repos, missing stow
adopt Move config files into a stow package and restow
version Print version
Options:
-c, --config PATH Config file path
-n, --dry-run Show what would be done without making changes
-p, --private Use private repo (for adopt command)
-v, --verbose Verbose output
Moves existing config files or directories into a stow package, then restows so the originals become symlinks managed by stow.
# Move a single directory
dotlayer adopt ~/.config/lazygit config
# Move multiple paths at once
dotlayer adopt ~/.config/lazygit ~/.config/lazydocker config
# Move into the private repo
dotlayer adopt --private ~/.config/lazysql config
# Preview without moving anything
dotlayer adopt --dry-run ~/.config/lazygit configThe last argument is always the package name. Dotlayer finds the first repo containing that package, or falls back to the first repo for new packages.
bundle exec rake test # run tests
bundle exec standardrb # lint
bundle exec rubycritic lib/ # code quality reportSee Contributing for full setup, testing, and contribution guidelines.
- Architecture — system design, data flow, and class responsibilities
- Contributing — setup, testing, and contribution guidelines