Personal dotfiles for a terminal-centric dev environment on macOS.
- Homebrew installed
gitinstalled
cd ~
git clone https://github.com/glnds/dotfiles.gitcd ~/dotfiles
brew install mise # one-time, seeds the task runner
mise run bootstrap # everything elsebootstrap chains: brew bundle (Brewfile) → symlinks into $HOME →
mise install (all mise-managed tools) → hk install (per-repo git
hooks from hk.pkl). Idempotent — safe to re-run.
Mise-managed tools also auto-install on first use
(not_found_auto_install = true in .config/mise/config.toml). Available
tasks: mise tasks.
Tip
If the shell ever reports mise: Unknown command, mise itself is gone —
re-seed it with brew install mise. update can't recover this: it runs
brew upgrade (installed formulae only), not brew bundle.
Make fish your default shell:
sudo bash -c 'echo /opt/homebrew/bin/fish >> /etc/shells'
chsh -s /opt/homebrew/bin/fishPeriodically update fish completions:
fish_update_completionsOpen nvim — LazyVim auto-installs plugins on first launch.
Two layers: brew bootstraps the system, mise handles everything else.
┌─ brew ────────────────────────────────────────────────────────────┐
│ Bootstrap only: git, mise, fish, neovim, tmux + GUI casks │
└───────────────────────────┬───────────────────────────────────────┘
│ brew bundle (one time)
▼
┌─ mise ────────────────────────────────────────────────────────────┐
│ Everything else, two scopes: │
│ │
│ GLOBAL PER-REPO │
│ ~/.config/mise/conf.d/*.toml <repo>/.mise.toml │
│ ────── ────── │
│ Always available Active only when cwd is │
│ starship, atuin, rg, bat, inside that repo │
│ fd, eza, jq, uv, rumdl, ... dotfiles → hk, pkl, │
│ trufflehog │
│ attracr → python, node, uv, │
│ ruff, hk, sam, … │
└───────────────────────────────────────────────────────────────────┘
Brewfile covers what must exist before mise runs, plus GUI casks and
formulae with no good mise plugin:
- Bootstrap:
git,mise - Shell/editor:
fish,neovim,luarocks,tmux,direnv - Utilities:
trash,tree,btop(no aqua-registry darwin/arm64 build) - Casks: Alacritty, nerd fonts, Finch, MarkEdit, LuLu, BlockBlock, KnockKnock, Malwarebytes
Note
Editing the Brewfile does nothing on its own. Only brew bundle reconciles
it — run mise run install (or mise run bootstrap) to apply new entries.
update runs brew upgrade, which only upgrades already-installed formulae
and never installs what you just added.
Pruning the other direction — after moving a tool from brew to mise — run
brew bundle cleanup --force. It uninstalls formulae/casks not in the
Brewfile (plus their orphaned deps), so the brew copy stops shadowing the
mise shim. brew leaves should then equal the Brewfile.
Declared in .config/mise/conf.d/*.toml, all pinned to latest. Available
in every shell, no matter the cwd:
20-shell.toml— starship, atuin, zoxide, fzf30-cli.toml— claude, gh, jq, ripgrep, bat, fd, eza, glow, dust, yazi, gitui, delta, rumdl, uv
Each repo ships its own .mise.toml declaring tools specific to that
project. This repo's pins hk, pkl, and trufflehog — useless outside
the dotfiles repo (git hook runner, its config language, secret scanner
called from the pre-commit hook). Other repos pin what they need: a
Python project pins python + uv + ruff; an AWS project pins
aws-sam-cli + cfn-lint + cfn-guard. When you cd into the repo,
mise puts those tools on PATH; when you leave, they're gone.
$ cd ~ # no .mise.toml in scope
$ hk --version
mise ERROR No version is set for shim: hk
$ cd ~/dotfiles # .mise.toml declares hk
$ hk --version
hk 1.46.0Why this matters:
- No global pollution. A project pinned to Python 3.11 doesn't fight
another pinned to 3.13. Each has its own
.mise.toml; both Just Work. - Reproducible across machines.
.mise.tomlis in git, so a fresh clone gets the exact same versions aftermise install. - Fast onboarding. Clone,
mise install, done — no chasing down which tools the project expects.
- One declarative tool for languages and CLIs — add a tool by editing one toml line, commit, done
- Fast installs from precompiled binaries via the aqua registry
- Auto-install on first use; no
brew installround trips - Versions live in git, so machines stay in sync
Single command, runs brew + mise + uv in one go:
update # fish wrapper
mise run update # equivalentDefined in .config/mise/conf.d/99-tasks.toml, the task chains:
brew update && brew upgrade && brew cleanupmise upgrade— re-resolveslatestpins and installs newer versionsmise prune— removes the now-unused old versionsuv tool upgrade --all— upgrades Python tools installed viauv tool
- fish — shell with autosuggestions and syntax highlighting
- starship — fast, customizable cross-shell prompt
- atuin — SQLite-backed shell history with fuzzy search
- tmux — terminal multiplexer
- Alacritty — GPU-accelerated terminal
emulator (
Cmd+Shift+Mto toggle maximize)
- bat —
catwith syntax highlighting - eza — modern
lsreplacement - fd — fast
findalternative - dust — visual
dureplacement - ripgrep — fast
grepalternative - fzf — fuzzy finder
- zoxide — smarter
cd - trash — safe
rmto trash can
- git — latest Homebrew-managed version
- delta — syntax-highlighted git diffs with side-by-side view
- gitui — lightweight terminal
Git UI (aliased as
tig) - gh — GitHub CLI
- hk — per-repo git hook runner
(config in
hk.pkl, install withhk install). On Git 2.54+ hooks are config-based, not scripts in.git/hooks/— so an empty.git/hooks/is expected. Verify withgit config --get-regexp '^hook\.', notls .git/hooks/.
- Finch — open-source container tool (Docker alternative)
AWS tooling (
aws-cli,aws-sam-cli,cfn-lint,cfn-guard) lives in each AWS project's.mise.toml, not globally.
- jq — JSON processor
- btop — system monitor with CPU, memory, disk, network, and GPU stats
- yazi — fast terminal file manager
- glow — terminal Markdown renderer
- tree — directory listing
- direnv — per-directory environment variables
- rumdl — fast Markdown linter (CLI + LSP for nvim)
- uv — fast Python package manager
- LuLu — open-source firewall, blocks unknown outgoing connections
- BlockBlock — monitors persistence locations (launch daemons, login items)
- KnockKnock — scans for persistently installed software
- Malwarebytes — on-demand malware scanner
TruffleHog (secret scanner) lives in each hk-enabled repo's
.mise.toml, called from the pre-commit hook — see this repo'shk.pklfor an example.
| Keys | Directory |
|---|---|
gs |
~/source |
gk |
~/Desktop |
gd |
~/Downloads |
go |
~/Documents |
tmux monitor-activity is too noisy for Claude Code (fires on every
output line). Instead, bell-based notifications trigger only when
Claude needs input.
How it works: Claude Code hooks send a terminal bell (\a) on
Stop and permission_prompt events → tmux monitor-bell
highlights the window name in the status bar (bold red).
The hooks are configured in ~/.claude/settings.json (not tracked
in this repo).
The gh CLI supports multiple accounts natively (v2.40+). Combined with
direnv, account switching is automatic per directory.
gh auth login # work account
gh auth login # private account (stacks)Verify with gh auth status.
Create .envrc in each repos root:
Work repos (~/work/.envrc):
export GH_TOKEN=$(gh auth token --user your-work-username 2>/dev/null)Private repos (~/personal/.envrc):
export GH_TOKEN=$(gh auth token --user your-private-username 2>/dev/null)Then direnv allow inside each folder.
GH_TOKEN overrides gh auth switch, so gh automatically uses the right
account based on working directory — mirroring how SSH config works.
The 2>/dev/null variant dynamically pulls the current stored token
rather than a hardcoded value.
Caveat: if you re-auth an account, the token updates automatically
since the .envrc evaluates gh auth token on each shell entry.