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
- cmus
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/setupSet 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. Devbox setup never upgrades its GitHub binaries; run script/update for that.
| 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 = ~/.config/git/company.gitconfig -
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.
git update-index --skip-worktree ./symlinks/npmrcTo make changes in the future:
git update-index --no-skip-worktree ./symlinks/npmrcThe 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 -g 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 for cmus/
play-albums. 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:echo '/- auto_smb -nosuid,nobrowse' | sudo tee -a /etc/auto_master echo '/System/Volumes/Data/mnt/media -fstype=smbfs,soft,ro ://guest:@SERVER/media' | sudo tee /etc/auto_smb sudo automount -cv ln -s /System/Volumes/Data/mnt/media/music ~/Music/SERVER # Query a local copy of the beets DB; refresh it occasionally cp ~/Music/SERVER/beets/library.db ~/.config/beets/library.db
Set
LOCAL_ALBUM_DIR="$HOME/Music/SERVER/albums"in~/.profile.local, anddirectory: ~/Music/SERVERin~/.config/beets/config.yaml.
- 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.