Managed with rcm (macOS) and NixOS + home-manager (Linux).
Most macOS packages are in the Brewfile. NixOS packages are declared in nixos/modules/home.nix.
- Shell: fish + starship prompt
- Terminal: Ghostty (Everblush theme)
- Editor: Zed (primary), Neovim (lazy.nvim), vim (barebones fallback)
- Git: difftastic (structural diffs), SSH signing, git-lfs
- NixOS: niri compositor, Ptyxis terminal, handlr-regex URL dispatching
- macOS: Caps Lock → Escape (hidutil), Touch ID sudo, Dock/Finder defaults
- 1Password (not installed via Brew for security reasons)
- BetterDisplay
- OrbStack
# Install rcm and core tools
brew install rcm fish git
brew bundle
# Set fish as default shell
sudo sh -c 'echo $(which fish) >> /etc/shells'
chsh -s $(which fish)
# Install fish plugins
fish -c 'curl -sL https://git.io/fisher | source && fisher install jorgebucaran/fisher && fisher install jorgebucaran/hydro'
# Clone and link dotfiles
git clone git@github.com:aroman/dotfiles.git .dotfiles
rcup
# Build bat theme cache
bat cache --build
# Directories
mkdir -p ~/Projects
mkdir -p ~/Pictures/Screenshots
defaults write com.apple.screencapture location ~/Pictures/Screenshots
# Keyboard
defaults write -g ApplePressAndHoldEnabled -bool false
defaults write -g InitialKeyRepeat -int 15
defaults write -g KeyRepeat -int 2
# Mail
defaults write com.apple.mail AddressesIncludeNameOnPasteboard -bool false
# Appearance
defaults write -g AppleReduceDesktopTinting -bool yes
# Finder
defaults write NSGlobalDomain AppleShowAllExtensions -bool true
chflags nohidden ~/Library
/usr/libexec/PlistBuddy -c "Set :DesktopViewSettings:IconViewSettings:arrangeBy grid" ~/Library/Preferences/com.apple.finder.plist
/usr/libexec/PlistBuddy -c "Set :FK_StandardViewSettings:IconViewSettings:arrangeBy grid" ~/Library/Preferences/com.apple.finder.plist
/usr/libexec/PlistBuddy -c "Set :StandardViewSettings:IconViewSettings:arrangeBy grid" ~/Library/Preferences/com.apple.finder.plist
# Dock
defaults write com.apple.dock autohide -bool true
defaults write com.apple.dock autohide-delay -float 0
defaults write com.apple.dock persistent-apps -array
defaults write com.apple.dock show-recents -bool false
defaults write com.apple.dock ResetLaunchPad -bool true
defaults write com.apple.dock mineffect -string scale
defaults write com.apple.Dock showhidden -bool true
# Trackpad
defaults write com.apple.AppleMultitouchTrackpad Clicking -bool true
# Velja (disable App Nap so link routing stays fast)
defaults write com.sindresorhus.Velja NSAppSleepDisabled -bool true
# Touch ID for sudo (survives macOS upgrades)
# pam-reattach is needed for tmux sessions; ignore_ssh falls back to password for SSH
# See: https://sixcolors.com/post/2023/08/in-macos-sonoma-touch-id-for-sudo-can-survive-updates/
brew install pam-reattach
printf 'auth optional /opt/homebrew/lib/pam/pam_reattach.so ignore_ssh\nauth sufficient pam_tid.so\n' | sudo tee /etc/pam.d/sudo_local > /dev/nullReboot to apply everything: sudo shutdown -r now
On the new machine (fresh NixOS with nothing installed):
# Get git in a temporary shell
nix-shell -p git
# Clone dotfiles (HTTPS — no SSH keys yet)
mkdir -p ~/Projects
git clone https://github.com/aroman/dotfiles.git ~/Projects/dotfilesThen, from another machine that already has the repo:
# 1. Enable SSH on the fresh install so you can access it remotely
# On the new machine, edit the default NixOS config:
# sudo nano /etc/nixos/configuration.nix
# Add: services.openssh.enable = true;
# Then: sudo nixos-rebuild switch
# 2. Pull the hardware config directly
mkdir -p nixos/hosts/<hostname>
# Use the machine's IP (run `ip addr` on it to find it — mDNS likely won't work yet)
scp <user>@<ip>:/etc/nixos/hardware-configuration.nix nixos/hosts/<hostname>/
# 3. Create default.nix and home.nix (see existing hosts for reference)
# Add the host to nixos/flake.nix
# 4. Push
git add nixos/hosts/<hostname> nixos/flake.nix
git commit -m "Add <hostname> NixOS host config"
git pushFinally, on the new machine:
cd ~/Projects/dotfiles
git pull
sudo nixos-rebuild switch --flake ~/Projects/dotfiles/nixos#<hostname>After the first rebuild, SSH, git, and everything else from common.nix will
be available. Set up SSH keys and switch the remote:
# Generate a machine-specific key (name it after the hostname)
ssh-keygen -t ed25519 -C "<hostname>" -f ~/.ssh/<hostname>
# Add to GitHub (auth via browser)
gh auth login
gh ssh-key add ~/.ssh/<hostname>.pub -t "<hostname>"
# Create ~/.ssh/config.local to tell SSH which key to use
echo 'Host github.com
IdentityFile ~/.ssh/<hostname>' > ~/.ssh/config.local
chmod 600 ~/.ssh/config.local
# Switch remote to SSH
git remote set-url origin git@github.com:aroman/dotfiles.git
# Load the key into the agent (AddKeysToAgent will handle it after this)
ssh-add ~/.ssh/<hostname>
# Symlink dotfiles managed by rcm (scripts in ~/.local/bin, etc.)
rcup -K
# Build bat theme cache (for Everblush theme)
bat cache --build
# Set up git commit signing (key matches the SSH key)
echo '[user]
signingkey = ~/.ssh/<hostname>.pub' > ~/.gitconfig.local
# Add API keys for tools (ai-commit, etc.)
echo 'set -gx GEMINI_API_KEY "your-key-here"' >> ~/.config/fish/conf.d/secrets.fishFrom your main machine, set up passwordless SSH:
ssh-copy-id <hostname>git clone git@github.com:aroman/dotfiles.git ~/Projects/dotfiles
sudo nixos-rebuild switch --flake ~/Projects/dotfiles/nixos
rcup -KHosts are defined in nixos/hosts/. Rebuild alias: bake
Both NixOS machines back up /home/aroman daily to Backblaze B2 using restic. Backup health is monitored by Healthchecks.io — it alerts if a daily backup doesn't run.
Architecture: restic encrypts and deduplicates client-side, then uploads to B2. Both machines share one B2 bucket and one restic repo. Restic tags snapshots by hostname automatically.
B2 setup:
- Bucket:
aroman-backups(US West, private, no server-side encryption, no object lock) - Each machine has its own B2 application key scoped to only this bucket
- Keys are named
restic-wizardtowerandrestic-moonbinder
Retention: 7 daily, 4 weekly, 6 monthly snapshots.
Secrets (per-machine, not tracked in repo):
| File | Contents | Shared? |
|---|---|---|
/etc/restic/password |
Restic repo encryption password | Same on all machines |
/etc/restic/b2-env |
B2_ACCOUNT_ID and B2_ACCOUNT_KEY |
Different per machine |
/etc/restic/healthchecks-url |
Healthchecks.io ping URL | Different per machine |
All stored in 1Password. The directory and files should be chmod 700/600, owned by root.
Setting up a new machine:
sudo mkdir -p /etc/restic && sudo chmod 700 /etc/restic
# Use the SAME restic password as other machines (from 1Password)
sudo nano /etc/restic/password
# Create a new B2 application key scoped to aroman-backups bucket
# (B2 Console → App Keys → Add a New Application Key)
sudo nano /etc/restic/b2-env
# B2_ACCOUNT_ID=<keyID>
# B2_ACCOUNT_KEY=<applicationKey>
# Create a new Healthchecks.io check (Period: 1 day, Grace: 2 hours)
sudo nano /etc/restic/healthchecks-url
# https://hc-ping.com/<uuid>
sudo chmod 600 /etc/restic/password /etc/restic/b2-env /etc/restic/healthchecks-urlManual operations:
# Trigger a backup now
sudo systemctl start restic-backups-b2.service
# Watch backup progress
sudo journalctl -fu restic-backups-b2.service
# List snapshots
sudo bash -c 'set -a && source /etc/restic/b2-env && restic -r b2:aroman-backups --password-file /etc/restic/password snapshots'
# Restore a file
sudo bash -c 'set -a && source /etc/restic/b2-env && restic -r b2:aroman-backups --password-file /etc/restic/password restore latest --target /tmp/restic-test --include /home/aroman/path/to/file'A domain-based URL routing system that sends links to the right app and Chrome profile automatically. Think Velja for Linux.
any app
-> xdg-open
-> handlr-regex (registered as default x-scheme-handler/https)
-> regex match on URL domain
-> figma.com/* : figma-open (CDP script)
-> youtube.com/* : Chrome (Personal profile)
-> * : Chrome (magiccircle.studio profile)
Key files:
nixos/modules/home.nix-- handlr-regex package,.desktopentries, TOML config, MIME associationslocal/bin/figma-open-- Figma URL handler script~/.config/handlr/handlr.toml-- generated regex routing rules
handlr-regex is a Rust-based
xdg-open replacement that matches URLs against regex rules. A .desktop entry
registers it as the default handler for x-scheme-handler/http and
x-scheme-handler/https. When any app calls xdg-open with an http(s) URL,
handlr matches it against the rules in handlr.toml and dispatches to the
appropriate command.
Chrome profiles are selected via --profile-directory. Profile names map to
directory names under ~/.config/google-chrome/ (e.g. Default, Profile 1).
figma-open goes beyond simple URL dispatching. Figma runs as a Chrome --app
window with --remote-debugging-port=9222, exposing the
Chrome DevTools Protocol.
The script uses CDP over WebSocket (via websocat) to control the running Figma
instance:
Same-file navigation (e.g. jumping to a different artboard):
When the target URL has the same base path as the currently open file but a
different node-id query parameter, the script avoids a full page reload.
Instead, it extracts the node ID and calls Figma's Plugin API directly via
Runtime.evaluate:
const node = await figma.getNodeByIdAsync('12438:18221');
figma.currentPage.selection = [node];
figma.viewport.scrollAndZoomIntoView([node]);This is the same internal API that Figma plugins use -- it selects the node and scrolls the viewport to it instantly, with no reload.
Different-file navigation:
Uses CDP Page.navigate for a standard full navigation to the new file.
Figma not running:
Falls back to launching Chrome with the full --app, --user-data-dir,
--user-agent, and --remote-debugging-port flags.
Window focus:
After navigation, the script focuses the Figma window via
niri's IPC (niri msg action focus-window),
matching on the app_id prefix chrome-www.figma.com.
Edit the handlr.toml section in nixos/modules/home.nix. Rules are matched top-down,
first match wins:
# Route to a specific Chrome profile
[[handlers]]
exec = "google-chrome-stable --profile-directory=\"Profile 1\" %u"
regexes = ['https?://(www\.)?example\.com(/.*)?']Then rebuild: bake