Declarative, Git-based configuration management for heterogeneous Unix estates.
Manage a single repository with independent, versioned profiles across laptops, servers, workstations and containers. Deploy actual files with correct ownership and atomic rollbacks, optional transparent encryption, and a bidirectional workflow that lets you edit in place.
.git/
├── refs/heads/
│ ├── dotta-worktree # Empty branch (worktree anchor)
│ ├── global # Base configuration
│ ├── darwin # macOS-specific
│ ├── linux # Linux-specific
│ └── hosts/laptop # Host-specific
└── dotta.db # State database (manifest + metadata)
Dotta uses a Virtual Working Directory (manifest) as the authoritative source of deployment intent. This is a key architectural innovation that provides explicit staging and fast operations.
The Three-Tree Model (analogous to Git):
Git Branches (History)
↓
Manifest (Virtual Working Directory / Staging)
↓
Filesystem (Deployed Files)
How It Works:
- Profile enable → Reads Git branches, stages all files in manifest with status
pending_deployment - Status → Reads manifest (O(1) per file), compares to filesystem
- Apply → Queries manifest for
pending_deploymententries, deploys them, updates status todeployed
Manifest Table (in .git/dotta.db):
- Status tracking: Three-state machine (
pending_deployment,deployed,pending_removal) - Git references: Each entry stores
git_oid(commit hash) andcontent_hash(file content hash) - Always current: Updated immediately when profiles/files change (eager consistency)
- Indexed: SQLite indexes on status, profile, storage_path for instant queries
Benefits:
- Explicit staging: See exactly what will be deployed/removed before applying
- Performance: 10-100x faster status checks (no Git tree walks)
- Safety: Preview destructive operations (profile disable shows what will be removed/reverted)
- Correctness: Manifest never stale (automatically synchronized with Git)
Files are stored with location prefixes (home/, root/) in isolated Git orphan branches (one per profile), then deployed to their filesystem locations on demand. Each profile maintains independent version history.
This provides:
- Multi-machine flexibility - Profiles version independently; no forced synchronization
- System-level configs - Root-owned files deployed with correct ownership/permissions
- Security by default - Pattern-based transparent encryption for secrets
- Strong safety guarantees - Atomic deployments with state tracking prevent data loss
Profile branch "darwin":
home/.bashrc → deploys to: $HOME/.bashrc
home/.config/fish/config.fish → deploys to: $HOME/.config/fish/config.fish
root/etc/apcupsd/apcupsd.conf → deploys to: /etc/apcupsd/apcupsd.conf
.dotta/metadata.json → metadata for permissions/ownership
Prefix rules:
- Path starts with
$HOME→ stored ashome/<relative_path> - Absolute path → stored as
root/<relative_path>
The repository's main worktree always points to dotta-worktree (an empty branch). This prevents Git status pollution and keeps your repository clean. All profile operations use temporary worktrees.
Profile resolution follows a strict priority order:
- Explicit CLI (
-p/--profile flags) - Temporary override for testing - State file (
profilesarray in.git/dotta.db) - Persistent management viadotta profile enable
Enabled profiles are managed exclusively through dotta profile enable/disable/reorder commands. The state file is the single source of truth for which profiles are enabled on each machine.
When profiles are applied, later profiles override earlier ones following this precedence:
global- Base configuration<os>- OS base profile (e.g.,darwin)<os>/<variant>- OS sub-profiles (e.g.,darwin/work, alphabetically sorted)hosts/<hostname>- Host base profilehosts/<hostname>/<variant>- Host sub-profiles (e.g.,hosts/laptop/vpn, alphabetically sorted)
Example layering for a macOS laptop with work and vpn configs:
dotta apply→global→darwin/base→darwin/work→hosts/laptop/vpn- Files from later profiles override files from earlier profiles
Dotta organizes configurations into Git orphan branches (profiles):
global # Base configuration (all systems)
darwin # macOS base settings
darwin/work # macOS work-specific settings
darwin/personal # macOS personal overrides
freebsd # FreeBSD base settings
linux/server # Linux server-specific settings
hosts/laptop # Per-machine overrides
hosts/laptop/vpn # Machine-specific variants
Profiles support hierarchical organization for both OS-specific and host-specific configurations. Profiles are applied in layered order, with later profiles overriding earlier ones:
global- Universal base configuration<os>- OS base profile (darwin, linux, freebsd)<os>/<variant>- OS sub-profiles (sorted alphabetically)hosts/<hostname>- Host base profilehosts/<hostname>/<variant>- Host sub-profiles (sorted alphabetically)
This enables:
- Shared base configuration across all machines
- OS-specific base settings with contextual variants
- Host-specific overrides with role-based variants
- Predictable layering with alphabetical sub-profile ordering
Note: Due to Git ref namespace limitations, you cannot have both a base profile and sub-profiles with the same prefix (e.g., darwin and darwin/work cannot coexist). Use either the base profile OR sub-profiles, not both. The same limitation applies to host profiles.
Dotta uses explicit profile management with immediate staging to provide safe, predictable control over which configurations are deployed on each machine:
- Available Profiles: Exist as local Git branches - can be inspected, not automatically deployed
- Enabled Profiles: Tracked in
.git/dotta.dbwith files staged in manifest - participate in all operations (apply, update, sync, status)
# Enable profiles for this machine (shows explicit staging preview)
$ dotta profile enable global darwin
Staged 23 files for deployment from 'global'
Staged 47 files for deployment from 'darwin'
5 files override lower precedence (global)
# View enabled vs available profiles
dotta profile list
# Status shows staged files (fast: reads from manifest, no Git operations)
$ dotta status
Staged for deployment:
[pending] home/.bashrc (darwin)
[pending] home/.vimrc (global)
... (65 more)
# Apply deploys staged files
$ dotta apply
Deploying 68 files...
✓ Deployed 68 files
# Operations use enabled profiles
dotta apply # Deploys: global, darwin
dotta status # Checks: global, darwin (instant manifest lookup)
dotta sync # Syncs: global, darwinClone automatically enables detected profiles: global, OS base and sub-profiles (darwin, darwin/*), and host base and sub-profiles (hosts/<hostname>, hosts/<hostname>/*).
Explicit Staging Benefits:
- See exactly what enabling/disabling a profile will do before applying
statuscommand is 10-100x faster (reads manifest, not Git trees)- Safe profile changes with preview of removals and fallbacks
Dotta automatically preserves file metadata across machines:
- File permissions (mode) - captured during
add, restored duringapply - Ownership tracking (for
root/prefix files) - preserves user:group when running as root - Stored in
.dotta/metadata.jsonwithin each profile branch - Cross-platform compatibility - permissions mapped appropriately per OS
Example workflow:
# On source machine (as root for system files)
dotta add --profile linux /etc/systemd/system/myservice.service
# On target machine
dotta apply # Restores both content and permissions (0644, root:root)Dotta uses a Virtual Working Directory (manifest) to optimize and safeguard deployment:
- Three-state tracking - Files tracked through lifecycle:
pending_deployment→deployed→pending_removal - Explicit staging - See what will be deployed/removed before applying
- Pre-flight checks - Detects conflicts before making changes
- Overlap detection - Warns when files appear in multiple profiles
- Smart skipping - Avoids rewriting unchanged files (enabled by default)
- Conflict resolution - Clear error messages with
--forceoverride option - Fast status checks - Instant manifest lookup (O(1) per file, no Git tree walks)
- Always current - Manifest automatically synchronized when profiles or files change
Dotta's apply command performs complete synchronization:
# Deploy new/updated files AND remove orphaned files
dotta apply
# Preview what would change
dotta apply --dry-run
# Advanced: apply without removing orphaned files
dotta apply --keep-orphansUnlike many dotfile managers that only deploy from repository to filesystem, dotta supports the reverse:
# Update profiles with modified files from filesystem
dotta update
# Two-way sync with remote
dotta sync # fetch + push/pull with conflict detectionRun custom scripts at lifecycle points:
~/.config/dotta/hooks/
├── pre-apply # Before deploying files
├── post-apply # After deployment (reload services, etc.)
├── pre-add # Before adding files
├── post-add # After adding files
└── ...Fine-grained control over what gets tracked:
- CLI patterns (
--excludeflags) - Highest priority, operation-specific - Repository
.dottaignore- Version-controlled, shared with team - Profile-specific
.dottaignore- Extends baseline, can use!to override - Config file patterns - User-specific, not shared
- Source
.gitignore- Respects existing Git ignore files
# Edit shared ignore patterns
dotta ignore
# Edit profile-specific overrides
dotta ignore --profile darwin
# Quickly add a specific pattern to profile
dotta ignore --profile darwin --add 'somefile'
# Test if a path would be ignored
dotta ignore --test ~/.config/nvim/node_modulesEncrypt sensitive files at rest in Git using authenticated encryption:
# Enable encryption in config
[encryption]
enabled = true
auto_encrypt = [".ssh/id_*", ".gnupg/*", "*.key"]
# Explicitly encrypt specific files
dotta add --profile global ~/.ssh/id_rsa --encrypt
# Auto-encrypt based on patterns (config)
dotta add --profile global ~/.ssh/id_ed25519 # Encrypted automatically
# Override auto-encryption
dotta add --profile global ~/.aws/config --no-encrypt
# Manage encryption keys
dotta key set # Set/cache passphrase for session
dotta key status # Check encryption status and key cache
dotta key clear # Clear cached passphraseSecurity Features:
- Deterministic AEAD - SIV (Synthetic IV) construction for Git-friendly encryption
- Path-bound encryption - Files tied to specific storage paths (authenticated associated data)
- Per-profile key isolation - Each profile uses a derived encryption key
- Passphrase-based key derivation - No key files to manage (PBKDF2-based)
- Persistent key caching - Cache persists across commands (default: 1 hour, machine-bound)
- Automatic decryption - Transparent during
applyandshowoperations
Encryption Modes:
- Explicit - Use
--encryptflag to force encryption - Auto-encrypt - Configure patterns in
[encryption] auto_encrypt(e.g.,"*.key",".ssh/id_*") - Override - Use
--no-encryptto skip auto-encryption for specific files
Encrypted files are stored in Git with a magic header (DOTTA) and decrypted transparently during deployment. Master keys are cached at ~/.cache/dotta/session (machine-bound, auto-expires) for seamless multi-command workflows.
Automate system setup with per-profile bootstrap scripts that run during initial configuration:
# Create/edit bootstrap script for a profile
dotta bootstrap --profile darwin --edit
# List bootstrap scripts across profiles
dotta bootstrap --list
# Run bootstrap scripts (auto-detected profiles)
dotta bootstrap
# Run for specific profiles with auto-confirmation
dotta bootstrap --profile global --profile darwin --yes
# Preview what would execute
dotta bootstrap --dry-run
# Continue even if a script fails
dotta bootstrap --continue-on-errorBootstrap scripts are stored in .bootstrap within each profile branch and receive environment context:
DOTTA_REPO_DIR- Path to dotta repositoryDOTTA_PROFILE- Current profile being bootstrappedDOTTA_PROFILES- Space-separated list of all profilesDOTTA_DRY_RUN- Set to "1" during dry-run modeHOME- User home directory
Integration with clone:
# Clone prompts to run bootstrap if scripts detected
dotta clone git@github.com:user/dotfiles.git
# Auto-run bootstrap without prompt
dotta clone <url> --bootstrap
# Skip bootstrap entirely
dotta clone <url> --no-bootstrap
# After clone, apply profiles
dotta applyUse cases:
- Install package managers (Homebrew, apt, yum)
- Install system dependencies
- Configure OS preferences (macOS defaults, systemd)
- Clone additional repositories
- Generate SSH keys or certificates
- Set up development environments
- libgit2 1.5+
- libhydrogen (bundled) - For file encryption
- sqlite3 3.40+
- C11-compliant compiler (clang recommended)
- POSIX system (macOS, Linux, FreeBSD)
# Clone repository
git clone https://github.com/srevn/dotta.git
cd dotta
# Check dependencies
make check-deps
# Build
make
# Install (optional)
sudo make install PREFIX=/usr/local
# Or use directly from bin/
./bin/dotta --versionmake # Release build
make debug # Debug build with sanitizers
make clean # Remove build artifacts
make format # Format code with clang-format
make install # Install to PREFIX (default: /usr/local)
make uninstall # Remove installed files# Create and initialize repository
dotta init
# Add files to base profiles
dotta add --profile global ~/.bashrc ~/.vimrc
# Add OS-specific files (hierarchical)
dotta add --profile darwin/base ~/.config/fish/config.fish
dotta add --profile darwin/work ~/.ssh/work_config
# Add host-specific files
dotta add --profile hosts/$(hostname) ~/.local/machine_specific
# Enable profiles for this machine (explicit staging)
$ dotta profile enable global darwin/base darwin/work hosts/$(hostname)
Staged 12 files for deployment from 'global'
Staged 23 files for deployment from 'darwin/base'
Staged 8 files for deployment from 'darwin/work'
2 files override lower precedence (darwin/base)
Staged 3 files for deployment from 'hosts/laptop'
# View status (fast: reads from manifest)
$ dotta status
Staged for deployment:
[pending] home/.bashrc (global)
[pending] home/.vimrc (global)
[pending] home/.config/fish/config.fish (darwin/base)
... (43 more)
# Apply configurations (layers: global → darwin/base → darwin/work → hosts/hostname)
$ dotta apply
Deploying 46 files...
✓ Deployed 46 files
✓ Filesystem synchronized with manifest# Clone dotfiles repository (auto-detects, enables, and stages profiles)
$ dotta clone git@github.com:username/dotfiles.git
Cloning repository...
Detecting profiles...
Found: global, darwin, darwin/work, hosts/laptop
Enabling detected profiles...
Staged 23 files for deployment from 'global'
Staged 47 files for deployment from 'darwin'
Staged 12 files for deployment from 'darwin/work'
3 files override lower precedence (darwin)
Staged 8 files for deployment from 'hosts/laptop'
✓ Clone complete
✓ 90 files staged for deployment
# Cloning automatically:
# 1. Detects relevant profiles:
# - global (if exists)
# - OS base and sub-profiles (darwin, darwin/work, darwin/personal)
# - Host base and sub-profiles (hosts/<hostname>, hosts/<hostname>/*)
# 2. Fetches detected profiles
# 3. Enables them and stages all files in manifest
# Example auto-detection on macOS "laptop" with profiles:
# → Selects: global, darwin, darwin/work, hosts/laptop
# If no profiles match, you can fetch manually
dotta profile fetch work/project1 work/project2
# Run bootstrap scripts if present (prompts for confirmation)
dotta bootstrap
# Apply staged configurations
$ dotta apply
Deploying 90 files...
✓ Deployed 90 files
✓ Filesystem synchronized with manifest
# Or clone with auto-bootstrap
dotta clone <url> --bootstrap# Make changes to your configs
vim ~/.bashrc
# Update the profile
dotta update
# Sync with remote
dotta sync
# On another machine
dotta sync # Pull changes and applydotta init # Initialize new repository
dotta clone <url> # Clone existing repository
dotta remote add origin <url> # Add remotedotta profile list # Show enabled vs available profiles
dotta profile list --remote # Show remote profiles
dotta profile fetch <name> # Download profile without enabling
dotta profile enable <name> # Enables profile and stages all files for deployment
dotta profile disable <name> # Disables profile and shows removal/fallback preview
dotta profile validate # Check state consistencydotta add --profile <name> <file> # Add files to profile
dotta apply [profile]... # Deploy enabled profiles (or specified)
dotta apply # Deploy and remove orphaned files
dotta update # Update enabled profiles with modified files
dotta sync # Intelligent two-way sync of enabled profilesdotta status # Show deployment and sync status
dotta list # List all profiles
dotta list <profile> # List files in profile
dotta list <profile> <file> # Show commit history for a specific file
dotta diff # Show differences
dotta show <file> # Show file content from profiledotta remove --profile <name> <file> # Remove from profile
dotta remove --profile <name> --delete-profile # Delete entire profile
dotta revert <file> <commit> # Revert to previous versiondotta bootstrap [--profile <name>] # Run bootstrap scripts
dotta bootstrap --edit # Create/edit bootstrap script
dotta ignore # Edit .dottaignore
dotta ignore --test <path> # Test if path would be ignored
dotta key set # Set encryption passphrase
dotta key status # Check encryption status
dotta git <command> # Run git commands in repositoryConfiguration file: ~/.config/dotta/config.toml
[core]
repo_dir = "~/.local/share/dotta/repo" # Repository location
strict_mode = false # Fail on validation errors
[security]
confirm_destructive = true # Prompt before overwrites
confirm_new_files = true # Prompt for new file detection
[sync]
auto_pull = true # Auto-pull when behind
diverged_strategy = "warn" # warn, rebase, merge, ours, theirs
[ignore]
patterns = [ # Personal ignore patterns
".DS_Store",
"*.local",
]
[encryption]
enabled = false # Enable encryption feature (opt-in)
auto_encrypt = [ # Patterns for automatic encryption
".ssh/id_*", # SSH private keys
".gnupg/*", # GPG keys
"*.key", # Generic key files
".aws/credentials", # AWS credentials
]
session_timeout = 3600 # Cache timeout in seconds (0=always prompt, -1=never expire)
opslimit = 10000 # KDF CPU costDOTTA_REPO_DIR # Override repo_dir
DOTTA_CONFIG_FILE # Use different config file
DOTTA_EDITOR # Editor for bootstrap/ignore (fallback: VISUAL → EDITOR → vi/nano)Configure commit message templates with variable substitution:
[commit]
title = "{host}: {action} {profile}"
body = """
Date: {datetime}
User: {user}
Files: {count}
{action_past}:
{files}
"""Variables: {host}, {user}, {profile}, {action}, {count}, {files}, {datetime}, {target_commit}
Track which profiles exist on remote without downloading:
dotta profile list --remote # Show available remote profiles
dotta profile fetch linux # Download without activating
dotta profile enable linux # Enable it
dotta status --remote # Check sync stateEach profile can extend or override baseline ignore patterns:
# Baseline .dottaignore (applies to all profiles)
*.log
.cache/
# Profile darwin/.dottaignore (extends baseline)
!debug.log # Override: keep debug.log in darwin profile
.DS_Store # Additional: ignore .DS_Store in darwinOrganize profiles hierarchically for both OS-specific and host-specific configurations:
# OS-specific hierarchical profiles
darwin # macOS base (or use sub-profiles only)
darwin/work # Work-specific macOS settings
darwin/personal # Personal macOS overrides
linux # Linux base (or use sub-profiles only)
linux/desktop # Desktop environment configs
linux/server # Server-specific settings
# Host-specific hierarchical profiles
hosts/laptop # Laptop base (or use sub-profiles only)
hosts/laptop/office # Office network configs
hosts/laptop/vpn # VPN-specific settings
hosts/desktop # Desktop base
hosts/desktop/gaming # Gaming-specific configs
Hierarchical rules:
- Sub-profiles are limited to one level deep (
darwin/work✓,darwin/work/client✗) - Multiple sub-profiles are applied in alphabetical order
- Git limitation: Cannot have both base AND sub-profiles (see note above)
Bootstrap scripts automate system setup when cloning configurations to a new machine. Each profile can have its own bootstrap script in .bootstrap:
#!/usr/bin/env bash
set -euo pipefail
echo "Running darwin bootstrap..."
# Install Homebrew if not present
if ! command -v brew >/dev/null 2>&1; then
echo "Installing Homebrew..."
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
fi
# Install essential packages
brew install git fish neovim tmux
# Set macOS preferences
defaults write NSGlobalDomain ApplePressAndHoldEnabled -bool false
defaults write com.apple.dock autohide -bool true
echo "darwin bootstrap complete!"Environment variables available in scripts:
$DOTTA_REPO_DIR- Repository location$DOTTA_PROFILE- Current profile name$DOTTA_PROFILES- All profiles being bootstrapped$DOTTA_DRY_RUN- "1" if dry-run, "0" otherwise
Execution order: Bootstrap scripts run in profile layering order (global → OS → host), allowing base scripts to install dependencies for host-specific scripts.
Revert individual files to previous versions:
# Show file history
dotta list global <file>
# Revert to specific commit
dotta revert ~/.bashrc@abc123
# Revert with commit
dotta revert --commit -m "Fix config" ~/.bashrc@HEAD~1Dotta is designed for efficiency and scalability:
- Virtual Working Directory (Manifest) - Pre-computed deployment state eliminates expensive Git tree walks
- Streaming tree walks - Uses callback-based iteration; never loads entire repository into memory
- Size-first blob comparison - Checks file size before content (short-circuits on mismatch, uses mmap when needed)
- Three-tier deployment optimization - OID hash comparison → profile version check → content comparison (only if needed)
- Incremental operations -
updateprocesses only diverged files;applyuses smart skip with O(1) hashmap lookups - Content caching - Preflight safety checks populate cache reused during deployment (avoids redundant decryption)
- O(1) lookups - Hashmaps throughout for state, metadata, manifest, and file index operations
- Load-once, query-many - State and metadata loaded once per operation, queried via hashmap
- Indexed manifest queries - SQLite indexes on status, profile, and storage_path for instant lookups
Operations scale linearly with tracked files, not repository history depth.
[Specify your license]
- Repository: https://github.com/srevn/dotta
- Documentation: [Link to docs if available]
- Issues: https://github.com/srevn/dotta/issues
Built with libgit2 for robust Git operations. Inspired by YADM's simplicity and Chezmoi's declarative approach, but designed from the ground up for the orphan branch model.