This is only public so I can easily share it across machines. There is probably nothing of interest for you here ... or is there?
My usage is mostly terminal-based on MacOS, Debian Trixie over SSH, or sometimes Crostini (Debian) on Chromebook. Making heavy use of:
- Ghostty
- bash / tmux
- Neovim
- fzf / fd / ripgrep
- MPD with rmpc
git clone https://github.com/fortes/dotfiles.git
./dotfiles/script/setupOn Ubuntu (22.04 or later, which Coder devboxes and most devcontainers use), script/setup runs a lighter setup instead that only installs a few packages and links dotfiles, and is safe to run on every boot (requires root or sudo access). For the same on other pre-provisioned machines, like a Debian 13 (Trixie) devcontainer:
DOTFILES_DEVBOX=1 ./dotfiles/script/setupDevbox setup never upgrades its GitHub binaries or npm language servers; run script/update for that, with the same DOTFILES_DEVBOX=1 (or export it in ~/.profile.local).
Set DOTFILES_SKIP_PACKAGES="bash ssh" (for example) to leave stow packages alone, like bash to keep machine-provided shell startup files. Stowing bash backs up existing shell files, including .hushlogin, before linking them.
| Command | Behavior |
|---|---|
script/setup |
Provision the platform, install/update tools, and link configuration. |
script/stow |
Relink configuration without package installs. |
script/update |
Update user tools without linking or changing OS configuration. |
Run make test for shell checks and podman build -t dotfiles . to test Debian setup.
Once you've run setup, you'll still have to do the following universal manual steps (see platform-specific sections for more):
-
Generate the machine's SSH keys via 1Password, then add the key into GitHub and wherever else
-
Add any additional ssh keys into
~/.ssh -
If it's a server, may want to authorize your other public keys on the new machine:
ssh-import-id gh:fortes
-
Add your favorite servers into
.ssh/config.local -
Setup
.gitconfig.local:[user] name = Your Name email = xyz@abc.comIf you need to tweak any config based upon the directory path, do something like
[includeIf "gitdir:~/src/company/"] path = ~/.gitconfig.company -
Fill in the placeholders in tracked templates, for the parts you use:
rg --hidden -l REPLACE_ME stowed-files README.md
-
Log into Coding agents
# GitHub CLI client gh auth login # Launch Claude Code, which will take you through the flow claude # Launch codex CLI to log in codex # Launch Antigravity CLI agent agy
script/stow doesn't unstow packages that no longer exist in the repo. After pulling changes that delete a stowed-files/<pkg>/ directory, broken symlinks can linger in $HOME. To remove broken symlinks that point into the dotfiles tree:
find ~ -maxdepth 5 -type l 2>/dev/null \
| while read -r link; do
[ -e "$link" ] && continue
target=$(readlink "$link")
case "$target" in *dotfiles/stowed-files*) rm -v "$link" ;; esac
done./script/install_node_packages installs and updates Corepack through npm and enables its pnpm and Yarn shims in ~/.local/bin. These select the package-manager version declared by each project. Node and npm come from Homebrew on macOS or ./script/install_node on Linux; Corepack does not manage the Node version.
Native addons in npm globals are built for one Node major version. Debian's script/install_node rebuilds them on major upgrades. On macOS, run script/install_node_packages --rebuild after Homebrew upgrades Node to a new major.
Setup runs ./script/lock_local_files to mark tracked *.local files and
mpdscribble.conf as skip-worktree. To expose those edits in Git, then hide them
again:
./script/lock_local_files --unlock
git status --short
./script/lock_local_filesThis only changes Git's index flags; it doesn't change file contents. Avoid committing or stashing these files, since they can contain secrets.
If upstream changes block a pull, back up each affected file outside the repo before unlocking and restoring the tracked version. For example, from the repo root:
file=stowed-files/bash/.profile.local
backup=$(mktemp -d)
cp "$file" "$backup/local-config"
./script/lock_local_files --unlock
git restore --worktree -- "$file"
git pull --ff-onlyOnly continue past the backup command if it succeeds. If several files block the pull, back up and restore the tracked version of each before pulling. After the pull, even if it fails, restore your local copy and lock the files again:
cp "$backup/local-config" "$file"
./script/lock_local_filesCompare your local versions with upstream template changes afterward to pick up new settings. The backup directory contains secrets; remove it when finished.
The very first nvim launch on a new machine installs the plugins itself:
vim.pack.add() prompts to confirm, then downloads them and compiles the
treesitter parsers (which blocks for a minute or two, with progress). They are
usable straight away, but anything that already ran during that startup won't
pick them up — the file you opened has had its FileType autocmds fire before
any parser existed — so :restart once it finishes.
Afterwards, to update plugins, rebuild native components (the fzf sorter) and refresh parsers:
:lua vim.pack.update()
" Must do `:write` to accept the updates, then `:restart`Re-run this command whenever you pull updates that add or change plugins.
Dropping a plugin from init.lua stops it loading but leaves it on disk. To
clean those up:
:lua vim.pack.del(vim.iter(vim.pack.get()):filter(function(p) return not p.active end):map(function(p) return p.spec.name end):totable())- Log into sync accounts, extensions should automatically install
- Configure uBlock Origin
- Enable in private mode
- Enable cloud storage mode. Should do the following for you, but doesn't always work:
- Enable annoyances filters
- Add Bypass paywalls clean filter by copy-pasting the contents
- Log into sync accounts, extensions should automatically install
- Log into 1Password extension
-
Set up Touch ID, Apple account if required
-
Remove all the junk from the dock
-
Enable Night Shift and set to Sunset to Sunrise
-
Turn off Natural Scrolling
-
Increase keyboard repeat rate to max, delay to minimum
-
Change Globe key to Control dictation
-
Disable iCloud syncing of all but Find My Mac & Safari
-
Set
terminal.appprofile, send option as meta key -
May want to install command line tools manually in order to get
git:xcode-select --install -
Run
setup_mac -
Install apps by hand. They update themselves, which leaves Homebrew's casks stale, so setup skips them. Skip any that MDM already installed:
brew install --cask 1password claude codex-app firefox ghostty google-chrome obsidian rectangle # Optional brew install --cask antigravity antigravity-cli # Personal machines brew install --cask plexamp signal
-
Make sure keys repeat properly in apps that disable it by default:
- Antigravity:
defaults write com.google.antigravity ApplePressAndHoldEnabled -bool false - Obsidian:
defaults write md.obsidian ApplePressAndHoldEnabled -bool false - For other apps that have this issue, do the following:
# Get the app id osascript -e 'id of app "Cursor"' # Outputs something like `com.todesktop.xxxxx` defaults write com.todesktop.xxxxx ApplePressAndHoldEnabled -bool false
- Antigravity:
-
terminal.appsucks with colors, switch to Ghostty and pin it in the dock -
Make sure
Rectangle.appstarts on login -
Install the 1Password extension in Safari (others should sync automatically)
-
Create the Podman VM once with
podman machine init. Add--nowto also start it; after that, runpodman machine startwhen needed. -
Colima stays installed for projects that still need Docker. Run
colima startwhen needed (avoidbrew services start colimaso it doesn't run on login). Skippodman-mac-helper, which takes/var/run/docker.sockfrom Colima. -
If gaming, install battle.net/Steam via brew:
# May require this first in order to run Battle.net # (Steam has a beta that doesn't require Rosetta) softwareupdate --install-rosetta --agree-to-license brew install --cask battle-net steam # For battle.net, will need to manually run setup to install the app # open /opt/homebrew/Caskroom/battle-net/VERSION/Battle.net-Setup.app
-
Log into PlexAmp, and set
music-decoyto open it with the play button:defaults write com.lowtechguys.MusicDecoy mediaAppPath /Applications/Plexamp.app
-
Automount the media share at a fixed path. Unlike Finder mounts, it remounts on access and doesn't litter
/Volumes. macOS upgrades may reset/etc/auto_master; re-run the first command if so. Use the share's read-only login; guest access fails with the misleading "No locks available" (errno 77, an SMB auth error):echo '/- auto_smb -nosuid,nobrowse' | sudo tee -a /etc/auto_master echo '/System/Volumes/Data/mnt/media -fstype=smbfs,soft,ro,filemode=0644,dirmode=0755 ://media:REPLACE_ME@REPLACE_ME/media' | sudo tee /etc/auto_smb sudo automount -cv ln -s /System/Volumes/Data/mnt/media/music ~/Music/SERVER
-
Start MPD, which plays here using the server's library, then use
rmpcorplay-albums. Set the server's address in~/.mpd/mpd.conf.local, replacingREPLACE_ME.play-albumsturns beets paths into MPD ones by stripping beets'directory, so keep that at the server's music path (/data/media/music). MPD needs System Settings → Privacy & Security → Local Network access to reach the server, otherwise it fails with "No route to host". Homebrew upgrades may ask again:brew services start mpd
-
The proxy database doesn't share the server's playlists, so copy its
.m3ufiles into~/.mpd/playlistsfor rmpc's Playlists tab. Rerun after the server regenerates them. Large playlists load slowly, since MPD looks up each song on the server:mpd-sync-playlists
-
Offline,
mpd-offline onswaps MPD to the albums synced from the server into~/Music/albums, with its own queue;mpd-offline offgoes back to the server's library. Only local albums play, from rmpc, playlists orplay-albums. Scrobbles wait in mpdscribble's journal until the network returns. Offline mode doesn't survive a reboot: runmpd-offline onagain, ormpd-offline offto go back. -
Scrobble MPD plays to Libre.fm and ListenBrainz: fill in the
REPLACE_MEs in~/.mpdscribble/mpdscribble.conf. Like the.localfiles, edits stay out of git oncescript/lock_local_fileshas run (setup does it):chmod 600 ~/.mpdscribble/mpdscribble.conf brew services start mpdscribble -
Media keys and Control Centre's Now Playing control MPD through
mpd-now-playable. Setup writes its LaunchAgent; start it once and it runs at login after that:launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/local.mpd-now-playable.plist
- For servers, make sure to set up email delivery
- Should also set up unattended upgrades via
sudo dpkg-reconfigure unattended-upgrades - You may need to install
avahi-daemon,avahi-dnsconfd,avahi-utils, andlibnss-mdnsto get.localhostnames to resolve properly mergerfsif you want to do any pooling of drivescifs-utilsmay also be useful to have installed for mounting Windows shares
- All the steps from
Chromesection above - Set up "Night Light" if it didn't automatically sync
- Enable Linux, choose a larger disk size (20GB fine?). Double check which debian version it is via
grep VERSION_CODENAME /etc/os-release(should betrixie) - Run
script/setup(skips podman: Crostini is already a container) - Share
Downloadsfolder with Linux, then symlink vialn -s /mnt/chromeos/MyFiles/Downloads ~/downloads - Change terminal font by going to
chrome-untrusted://terminal/html/nassh_preferences_editor.html- Add
'DejaVu Sans Mono Nerd'to the beginning of "Text Font Family" - Add the following to custom CSS:
@font-face { font-family: 'DejaVu Sans Mono Nerd'; src: url(https://raw.githubusercontent.com/ryanoasis/nerd-fonts/master/patched-fonts/DejaVuSansMono/Regular/DejaVuSansMNerdFontMono-Regular.ttf); font-weight: normal; font-style: normal; }
- Add
Images are built with some frequency, via CI. Podman runs them rootless. On Linux, --userns=keep-id maps your user to the image's fortes user (UID 1000), so files written to /workspaces belong to you on the host. macOS's podman machine already maps mounted files to your user, so the flag is harmless there.
Docker works too: swap podman for docker and drop the --userns=… flag, since rootful Docker already maps UID 1000 straight through. On Debian, script/install_docker installs Docker (not run by setup, and skips the docker group, so use sudo docker).
On Debian, rootless containers stop when you log out. Run sudo loginctl enable-linger "$USER" to keep a long-running container alive.
# Start container in background
podman run -d -it --name dotfiles --userns=keep-id:uid=1000,gid=1000 \
-v ~/src:/workspaces ghcr.io/fortes/dotfiles:latest tmux new-session -s main
# Connect to tmux session
podman exec -it dotfiles tmux attach -t main
# Stop/restart as needed
podman stop dotfiles
podman start dotfilesGets deleted when you exit.
podman run -it --rm --name dotfiles --userns=keep-id:uid=1000,gid=1000 \
-v ~/src:/workspaces ghcr.io/fortes/dotfiles:latestPreserves state, but not always running
# First time
podman run -it --name dotfiles --userns=keep-id:uid=1000,gid=1000 \
-v ~/src:/workspaces ghcr.io/fortes/dotfiles:latest
# Later sessions
podman start -ai dotfilespodman build -t dotfiles .Then follow normal pattern, just use the local image name like so:
podman run -it --rm --name dotfiles --userns=keep-id:uid=1000,gid=1000 dotfilesSince you have to manually install packages from backports, can be tricky to know what is available. To find out, run the following:
# Installed packages with a newer version in backports
sudo apt-get -s upgrade -t "$(. /etc/os-release && echo "$VERSION_CODENAME")-backports" \
| grep Backports
# Or the full picture for one package
apt-cache policy neovimThis will list out all the packages installed, then need to search through to manually check which have backports available (pipe to nvim -).
This repository is licensed under the BSD 3-Clause License. See the LICENSE file for more information.