Personal dotfiles repository built and managed using the Chezmoi declarative configuration manager. Supports one-key deployment and lifecycle initialization across macOS and Linux (GUI/Headless) environments.
The bootstrap script detects your platform, installs the three base utilities (git, chezmoi, age), and then runs chezmoi init --apply. Remaining system packages (including fcitx5-rime) are installed during apply from linux-packages.txt via pm.sh:
git clone https://github.com/icyleaf/dotfiles.git ~/.dotfiles
sh ~/.dotfiles/install.shImportant
Restore the shared Age private key before the first chezmoi apply (see Secret Management), otherwise encrypted secrets will be skipped with warnings.
Directly initialize and apply without manually cloning the repository:
sh -c "$(curl -fsLS chezmoi.io/get)" -- init --apply icyleafWarning
This path does not run install.sh. Ensure git, chezmoi, and age are already installed, and restore the Age key first, or secret deployment will be skipped.
If you have already cloned this repository locally:
cd ~/.dotfiles
chezmoi init --source "$PWD" --applyNote
During the first init process, Chezmoi will interactively prompt for your Git name, email, and whether the current system is a GUI-less Headless environment, and automatically render the configurations accordingly.
dot_config/: Generic and Linux-specific application configurations (e.g., Hyprland, Waybar, Walker, etc.) deployed to~/.config/.dot_local/bin/: Custom executables and maintenance scripts deployed to~/.local/bin/.Library/: macOS-specific preference files (e.g., Alfred, iTerm2, LinearMouse, etc.) deployed to~/Library/.assets/: Static non-dotfiles repository assets, such as Plymouth themes.
Before submitting configuration changes, you can run the integration test script locally to ensure template rendering and installation lifecycles function correctly:
# Run non-intrusive sandbox testing
./.scratch/verify-chezmoi.shSensitive private data (SSH keys, environment variables) is managed using Age encryption via Chezmoi's encryption integration and custom lifecycle hooks.
Each machine has its own Age key pair. The active identity is resolved at
chezmoi init time as ~/.local/share/age/<machine_profile>.txt when present,
otherwise the shared ~/.local/share/age/default-key.txt. To use an existing
key, place it at the resolved path before the first apply:
mkdir -p ~/.local/share/age
cp /path/to/your/backup/<profile>.txt ~/.local/share/age/ # or default-key.txt
chmod 600 ~/.local/share/age/<profile>.txtIf no key is present, run_once_before_setup-age-key.sh generates one at the
resolved path. A newly generated key cannot decrypt existing repository
secrets — restore the matching private key from backup, or add its public key to
secrets/recipients.txt and re-encrypt (see below).
secrets/base/**— shared by every machine; encrypted to all recipients listed insecrets/recipients.txt(public keys only, safe to commit).secrets/profiles/<profile>/**— readable by the owning machine alone; encrypted to that machine's key.
- On the new machine, print its public key:
age-keygen -y ~/.local/share/age/<profile>.txt
- Append the public key to
secrets/recipients.txt. - Re-encrypt the shared secrets so the new machine can read them:
scripts/reencrypt-secrets.sh # uses ~/.local/share/age/default-key.txt scripts/reencrypt-secrets.sh /path/to/key.txt # or an explicit identity
- Commit
secrets/recipients.txtand the re-encryptedsecrets/base/**.
age -R secrets/recipients.txt -o secrets/base/foo.age fooEncrypt to that profile's public key alone:
age -r <profile_public_key> -o secrets/profiles/<profile_name>/foo.age foo# Shared secret
age -d -i ~/.local/share/age/default-key.txt secrets/base/foo.age > /tmp/foo
nano /tmp/foo
age -R secrets/recipients.txt -o secrets/base/foo.age /tmp/foo
rm /tmp/fooFor a profile secret, decrypt with that machine's identity and re-encrypt with
age -r <profile_public_key>.
- Create a directory for the new profile:
mkdir -p secrets/profiles/<profile_name>/ssh
- Encrypt the profile's environment variables to that profile's key:
age -r <profile_public_key> -o secrets/profiles/<profile_name>/local.zsh.age local.zsh
- Commit the encrypted
.agefiles (never commit the plaintext versions).
SSH Host entries live in encrypted, reusable fragments instead of a single
plaintext ~/.ssh/config:
secrets/base/ssh_config.d/<group>/<fragment>.conf.age— shared fragments, grouped for organisation (common,homelab,tokyo,vps,work_wst). Every group is deployed to every machine and decrypted to~/.ssh/config.d/<group>_<fragment>.conf. A fragment the active age identity cannot decrypt is skipped. The group name only namespaces the deployed filename.secrets/profiles/<profile>/ssh_config.d/<fragment>.conf.age— fragments exclusive to one profile, decrypted to~/.ssh/config.d/<profile>_<fragment>.conf.
The managed, plaintext ~/.ssh/config (source: private_dot_ssh/config) holds only
Include config.d/*.conf plus the global Host * defaults.
run_onchange_deploy-secrets.sh regenerates ~/.ssh/config.d/*.conf on every
apply and removes stale fragments, so deleting a .age file also removes its
deployed config.
To add a host:
# 1. Create a plaintext fragment (e.g. secrets/base/ssh_config.d/homelab/20_db.conf)
vim secrets/base/ssh_config.d/homelab/20_db.conf
# 2. Encrypt it into the group, to every recipient like other base secrets
age -R secrets/recipients.txt \
-o secrets/base/ssh_config.d/homelab/20_db.conf.age \
secrets/base/ssh_config.d/homelab/20_db.conf
rm secrets/base/ssh_config.d/homelab/20_db.conf # never commit plaintext
# 3. Deploy
chezmoi apply