- Shell 50.5%
- Lua 27.5%
- HTML 17%
- Python 5%
dotfiles
Welcome to the jungle that is my collection of dotfiles.
GNU Stow-managed like the grownups do.
Packages
bat->~/.config/bat/configclaude->~/.claude(only the actual config is tracked — see Per-package Notes)codex->~/.codex/AGENTS.md(loads the canonical instructions and persona from theclaudepackage)eza->~/.config/eza/theme.ymlghostty->~/.config/ghostty/configand~/.local/bin/ghostty-tmuxgit->~/.gitconfiggnupg->~/.gnupg/gpg-agent.confand~/.local/bin/pinentry-resolvejosm->~/.config/JOSM/preferences.xmland~/.local/share/applications/org.openstreetmap.josm.desktopmise->~/.config/mise/config.tomlnvim->~/.config/nvim(Neovim 0.11 IDE configuration)tmux->~/.tmux.confand~/.config/tmuxzsh->~/.zshrcand~/.config/zsh/claude.zshzed->~/.config/zed/settings.jsonand~/.config/zed/keymap.jsonalacritty->~/.config/alacritty(not using anymore, keeping around in case I change my mind.)
Install
Clone with submodules (soma, the SomaFM zsh plugin sourced by zsh/.zshrc):
git clone --recurse-submodules <repo-url> ~/dotfiles
Then from the repository:
stow --target="$HOME" alacritty bat claude codex eza ghostty git gnupg josm mise nvim tmux zsh zed
Then mise install to pull the pinned tool versions (see mise).
On macOS, JOSM normally reads preferences from ~/Library/Preferences/JOSM.
To use the stowed XDG-style config, either point JOSM at it when launching:
java -Djosm.pref="$HOME/.config/JOSM" -jar josm.jar
or replace the macOS preference directory with a symlink after checking that it does not contain local-only settings you want to keep:
mv "$HOME/Library/Preferences/JOSM" "$HOME/Library/Preferences/JOSM.bak"
ln -s "$HOME/.config/JOSM" "$HOME/Library/Preferences/JOSM"
Updating on an already-set-up machine
git pull only updates the files inside the repo —
it does not touch the symlinks in $HOME.
Stow creates those, and it only relinks when you run it.
Editing an existing already-linked file needs nothing
(the symlink points at it).
But when a pulled commit adds a new file or directory to a package,
that new path has no symlink yet, so anything referencing it breaks —
e.g. .zshrc sourcing a freshly-added ~/.config/zsh/claude.zsh fails with
no such file or directory until you relink.
Re-stow the affected package to create the missing links:
stow -R zsh # -R = restow: remove and recreate this package's links
Stow folds correctly into existing directories, so this is safe.
If it prints a would cause conflicts message,
that's stow refusing to clobber a real (non-symlink) file —
move that file aside or use --adopt after checking the diff,
don't force past it.
Dependencies
Common:
- GNU Stow
- Bash
- tmux
- Ghostty
- IosevkaTerm Nerd Font
mpg123,curl,jqfor thesomaplugin (SomaFM player)- GnuPG (
gpg,gpg-agent) plus a pinentry backend —pinentry-macon macOS,pinentry-gnome3orpinentry-curseson Linux — for commit signing (seegit/gnupg) - mise itself — installs the tools it pins, but isn't in its own registry, so install it via its own instructions first
Linux clipboard support uses wl-copy, xclip, or xsel,
in that order. macOS clipboard support uses pbcopy.
Linux / GNOME / Wayland Notes
These are machine setup notes, not Stow-managed configuration.
GNOME stores settings such as custom keyboard shortcuts in its dconf database,
while APT repository configuration lives under /etc;
neither should be symlinked into the home directory.
1Password Quick Access
1Password's in-app global shortcut does not work directly under GNOME Wayland. Create a GNOME custom keyboard shortcut instead:
- Name:
1Password Quick Access - Command:
/usr/bin/1password --quick-access - Shortcut:
Ctrl+Shift+Space
This can be configured in Settings → Keyboard → Keyboard Shortcuts → View and Customize Shortcuts → Custom Shortcuts. It follows 1Password's official Linux shortcut guidance.
Firefox on Ubuntu
Ubuntu's firefox APT package may be a transition package for the Snap build.
This machine instead uses Mozilla's official .deb repository, following
Mozilla's installation instructions:
- repository:
https://packages.mozilla.org/apt - signing-key fingerprint:
35BAA0B33E9EB396F59CA838C0BA5CE6DC6315A3 - APT pin priority:
1000for originpackages.mozilla.org - default browser desktop entry:
firefox.desktop
The repository key, source, and pin live in /etc/apt/keyrings/,
/etc/apt/sources.list.d/, and /etc/apt/preferences.d/;
they are deliberately not tracked here.
Graphical programs launched from tmux
A long-running tmux server can retain stale desktop-session state.
GUI launchers under Wayland need DBUS_SESSION_BUS_ADDRESS, WAYLAND_DISPLAY,
XDG_CURRENT_DESKTOP, and XDG_RUNTIME_DIR.
The tmux package includes these in update-environment,
so attaching a client from the current desktop session refreshes them.
If a graphical command still silently fails
after logging into a new desktop session,
detach and reattach the tmux client before debugging the application itself.
Per-package Notes
claude
~/.claude mixes real config with Claude Code's own runtime state
(OAuth credentials, session transcripts, caches, sqlite dbs).
.gitignore ignores claude/.claude/* by default
and only allows the actual config through
(CLAUDE.md, PERSONA.md, settings.json, statusline-command.sh, skills/)
— don't widen that allowlist to the whole tree again,
it's what keeps things like .credentials.json from being stageable at all.
settings.json's statusLine.command uses $HOME, not a hardcoded path —
keep it that way or the statusline silently stops rendering on whichever machine
doesn't match the hardcoded one.
git, gnupg
Commit signing config.
git/.gitconfig sets user.signingkey and commit.gpgsign/tag.gpgsign.
gnupg/.gnupg/gpg-agent.conf sets cache TTLs
and points pinentry-program at gnupg/.local/bin/pinentry-resolve,
a small wrapper that picks pinentry-mac on macOS
or pinentry-gnome3/pinentry-curses on Linux,
so the same conf file works unmodified on both.
Only config is versioned —
the actual private key stays local under ~/.gnupg/private-keys-v1.d,
untouched by stow or git.
On a new machine, import your own key (gpg --import) and trust it
(gpg --edit-key <id> -> trust -> 5)
before signing will work; this repo has no key material to bootstrap from.
josm
The josm package ships a sanitized preferences.xml with the OSM OAuth access
token stripped, so JOSM will prompt you to log in once per machine.
Do not commit your live token back into the repo.
It also ships a local desktop entry
(~/.local/share/applications/org.openstreetmap.josm.desktop) that overrides
the system one to set JAVA_OPTS=-Dsun.java2d.uiScale=1.5 for hi-dpi screens on
Linux — adjust the scale factor per machine, or delete the package's copy if you
don't need it.
mise
Pins jq and fzf (both referenced elsewhere in this repo —
the soma plugin and statusline script use jq,
.zshrc and the tmux open-url.sh script use fzf) plus node, gh,
and uv.
Run mise install after stowing to actually pull the pinned versions —
stowing only puts the config in place.
The manifest also installs the language servers and formatters shared by Zed
and Neovim: stylua, lua-language-server, shellcheck, shfmt, ruff,
ty, basedpyright, prettier, taplo, marksman, and the Bash, YAML,
and VS Code HTML/JSON language servers.
curl, mpg123, stow, GnuPG, Ghostty, and Docker aren't in mise's registry
(or, for Docker, need a real daemon rather than a version-managed binary)
and stay OS-package-manager territory; see
Dependencies.
zsh completions
Tab completion only works for a command
if a _toolname completion function is sitting in $fpath by the time
compinit runs — that happens for free for tools packaged by the OS or Homebrew
(git, npm, etc.), but not for tools that generate their own completion
script on demand (mise, gh, uv, docker).
.zshrc evals each of those explicitly,
right after compinit/mise activate,
guarded by (( $+commands[...] ))
so a missing tool doesn't break shell startup.
Add new tools to that block the same way, not to fpath — compdef,
which these generated scripts call, only exists once compinit has already run.
alacritty
The config contains a toggle script to switch between Gruvbox light and dark.
This happens by copying one of the theme files to active.toml
which is what the alacritty config imports after the dark default.
The active.toml file is gitignored here.
tmux
The statusline uses ANSI indices (colour0-15) so it follows the terminal theme
(Ghostty's Gruvbox Material Dark).
Pane dimming and the statusline bg use fixed 256-cube greys
(colour234/colour237), tuned against that dark theme — revisit if switching
to a light theme.
default-shell is pinned explicitly per OS
(/bin/zsh on macOS, /usr/bin/zsh on Linux)
rather than inherited from $SHELL — some launch paths
(e.g. Ghostty's wrapper)
don't reliably set $SHELL to zsh,
and a pane silently running bash never sources .zshrc.
eza
eza has no config file for default flags; theme.yml only controls colors
(loaded via EZA_CONFIG_DIR, exported in the zsh package).
The git-status column comes from the --git flag on the long-format aliases in
zsh/.zshrc (ll, la).
Grid ls doesn't show git — the column only renders in long mode.
zsh
Framework-free (no oh-my-zsh); everything lives in .zshrc,
plus the sourced .config/zsh/claude.zsh and the soma submodule.
brew shellenv is guarded
([ -x /opt/homebrew/bin/brew ]) so it no-ops on Linux instead of erroring.
Postgres.app and the Zed application alias are likewise enabled only on macOS
when their binaries exist.
PNPM_HOME uses ~/Library/pnpm on macOS and the XDG data directory
(~/.local/share/pnpm by default) on Linux.
claude.zsh (sourced from .zshrc via ~/.config/zsh/claude.zsh) provides
claude-personal and claude-work aliases for switching between two Claude.ai
subscription accounts on macOS.
Tokens are stored in macOS Keychain — not in this repo.
The aliases and the Keychain-backed Codeberg token are not configured on Linux.
After adopting the dotfiles on a new machine,
generate a fresh token per account and store it:
# While logged into the account (claude /login first if needed):
claude setup-token
security add-generic-password -a claude-personal -s claude-tokens -w "paste-token-here"
# Repeat for the work account with -a claude-work
Do not transfer tokens between machines — regenerate them. Each token lasts one year.
Git hooks
A tracked pre-commit hook in .githooks/ refuses commits
that would leak an OSM OAuth access token from a JOSM preferences.xml
(JOSM rewrites that file with your live token on exit).
core.hooksPath is local git config and is not set by git clone,
so enable it once per clone:
git config core.hooksPath .githooks
If these files already exist outside Stow,
move them aside first or use stow --adopt carefully after checking the diff.