macOS switched to zsh as the default shell in Catalina (2019). These dotfiles were originally bash-focused using Bash-It framework. Time to modernize.
- zsh-autosuggestions - Fish-like suggestions as you type
- zsh-syntax-highlighting - Command highlighting
Ghostty is the primary terminal (~/.config/ghostty/config, chezmoi-managed).
iTerm2 stays installed as a secondary for tmux -CC control mode; its prefs
plist is intentionally not managed yet. Revisit if it earns a permanent seat.
- Primary config:
~/.zshrc(was~/.bash_profile) - History:
~/.zsh_historywith zsh-specific options - Completions: Native zsh completion system
- Homebrew location:
/opt/homebrew(was/usr/local) - Universal path handling for both architectures
bash/ → shell/ # Shell-agnostic configs
├── exports.bash ├── exports.sh
├── path.bash ├── path.sh
├── aliases.bash ├── aliases.sh
└── functions.bash └── functions.sh
Old bash-specific files removed (git history preserved).
| Old | New | Why |
|---|---|---|
| Fresh | chezmoi | Per-machine templating; no build step; active project |
| nvm | mise | Polyglot, fast, no shell-startup penalty |
| Bash-It themes | Starship | Cross-shell, fast, configurable |
| fasd | zoxide | Faster, maintained, better algorithm |
| hub | gh | Official GitHub CLI |
| reattach-to-user-namespace | (removed) | Not needed since tmux 2.6 |
| Python 2 http.server | Python 3 | Python 2 EOL |
| Tool | Purpose |
|---|---|
| bat | Better cat with syntax highlighting |
| fd | Better find |
| ripgrep (rg) | Better grep |
| fzf | Fuzzy finder |
| git-delta | Better git diff |
| rtk | Token-optimizing CLI proxy — always on PATH; automatic rewriting is a toggle (see below) |
Removed deprecated packages and trimmed to essentials:
boot2docker,docker-machine,fig(old Docker tooling)reattach-to-user-namespace(tmux workaround)mongodbwith args (needs tap now)lighttable,lightpaper(discontinued editors)growl(deprecated)- Old cask tap names (
caskroom/*→homebrew/*)
Retired Fresh in favor of chezmoi
(mode = "symlink"). GNU Stow was considered and rejected — the recurring pain in
this repo is per-machine state (mise config.local.toml, git user identity,
future work/personal split), which chezmoi templates solve natively and Stow does
not. See CHEZMOI_MIGRATION.md for the full rationale and cut-over log.
How it maps (source name → target):
dot_zshrc→~/.zshrc;.zshrcsourcesshell/*.shdirectly from$DOTFILES(those helpers stay outside chezmoi's tree via.chezmoiignore).dot_gitconfig.tmpl→~/.gitconfig(templated; folds in user identity from chezmoi data — no more.gituserconfigstub orinclude.pathhack).dot_tmux.conf→~/.tmux.conf(source-files the vendored seebi solarized theme).dot_config/starship.toml,dot_config/ghostty/config,dot_inputrc→ their~paths.dot_config/mise/conf.d/fresh.toml→ managed symlink (shared tool baseline);create_config*.toml.tmpl→ seeded once, then edit-freely (chezmoi won't clobber).private_bin/→~/bin(executable_cross_origin_chrome,symlink_subl,symlink_dotfiles).private_dot_emacs.d/init.el→~/.emacs.d/init.el.- seebi color files vendored into the repo (
shell/dircolors.ansi-universal,tmux/tmuxcolors-dark.conf) — chezmoi can't fetch remote git like Fresh did.
Daily use: chezmoi status / chezmoi diff / chezmoi apply / chezmoi edit.
Per-machine answers (name/email/work/machine) live in ~/.config/chezmoi/chezmoi.toml
(prompted by chezmoi init, not committed).
mise owns the CLI toolchain (node, rust, go, bun, starship,
ripgrep, chezmoi itself, …). Shared baseline: ~/.config/mise/conf.d/fresh.toml
(committed). Machine-local: ~/.config/mise/config.toml (seeded once). Python is
delegated to uv. eval "$(mise activate zsh)" runs from .zshrc.
settings.python.uv_venv_auto activates a uv project's .venv on cd (creating it
if absent), so source .venv/bin/activate is no longer needed. mise finds the project
by walking up for uv.lock — a bare .venv is ignored, so run uv sync once in a new
project before it takes effect.
git clone https://github.com/simshanith/dotfiles.git ~/.dotfiles
cd ~/.dotfiles
./install.sh
exec zshrtk is a token-optimizing CLI proxy. It installs fleet-wide via the mise
baseline and is always available to invoke directly — rtk read foo works on
every machine. What is optional, and off by default, is the Claude Code hook
that rewrites Bash commands before you see them.
mise run rtk:hook # enable: allowlisted commands get rewritten
mise run rtk:unhook # disable: nothing is rewritten automatically
mise run rtk:hook:status # what is actually wired, and any drift
mise run rtk:uninstall # remove rtk entirely (dry run; pass -- --yes)All four wrap ~/bin/rtk-hook and ~/bin/rtk-uninstall, which are runnable
directly. Both directions back up every file they touch and are safe to re-run.
Why a toggle rather than a decision. rtk substitutes rather than filters: it drops the wrapper and any argument it does not recognize.
pnpm lint→ ran rtk's own eslint instead of the project'slintscriptpnpm exec prettier→ resolvedprettieroffPATH, notnode_modules/.binnext build→ droppedbuildoutright- a rewritten
grepcould hit a usage error rtk swallowed with exit 0, so a search silently returned nothing
Those are correctness bugs rather than overhead, and one silently-wrong result
costs more than the filtering saves. But the savings are real too — rtk gain
showed 4.7M tokens over 90 days, heavily concentrated in cat/head/tail.
Both facts are true, and which one dominates depends on the machine and the
week, so the posture is a switch rather than a verdict baked into the repo.
Invoked deliberately, none of that ambiguity exists: you asked for rtk read,
so you get rtk read. The hazard was always automatic substitution.
The allowlist. When enabled, the Bash hook points at
~/bin/rtk-allowlist-hook rather than rtk's own rtk hook claude. rtk ships
only denylists (exclude_commands, transparent_prefixes), so every handler is
armed by default and each release re-arms the surface — 0.43.0 added three new
rewrite paths with no action on our side. The gate inverts that default: a
command is rewritten only if ~/.config/rtk-allowlist.toml names it, and only
as the head of a single simple command. Anything with a pipe, &&, ;, or a
redirect passes through untouched.
The list is short on purpose — cat head tail ls gh find wc du
ps vitest cargo, plus git commit and git fetch — each earning its place
in rtk gain. The whole JS toolchain is deliberately absent.
The gate is a uv run --script script (requires-python = ">=3.11"), so uv
owns the interpreter and tomllib is always there — no vendored TOML parser
for old Pythons. rtk-hook enable refuses to wire it if uv is off PATH.
How the toggle sticks. The choice is a marker file
(~/.config/rtk-hook-enabled), per-machine and uncommitted. chezmoi apply
runs run_after_20-claude-rtk-hook.sh, which reconciles to the marker rather
than asserting the hook: enabled machines get their wiring repaired if it
drifted, disabled machines are left alone. Without that gate a chezmoi apply
would silently undo rtk-unhook. An absent marker means disabled, so a fresh
machine gets rtk on PATH and no hook until it asks — the right default for a
fleet that is mostly headless.
Migrating a machine that is already wired. A machine hooked before the
toggle existed has no marker, so rtk:hook:status reports a hook with no
recorded intent and does nothing about it — chezmoi apply leaves such a
machine exactly as-is. Run mise run rtk:hook once to keep the hook and record
that choice, or mise run rtk:unhook to drop it.
This also means rtk init -g is never run. It repoints the hook straight at
rtk hook claude, re-arming everything, and overwrites ~/.claude/RTK.md.
rtk-hook status reports that drift if it happens.
Stats. rtk-hook disable --reset-history wipes history.db so rtk gain
restarts from zero. Worth doing when going opt-in: an existing database is
mostly hook-driven traffic, which answers "did the hook save tokens?" — a
question that stops mattering once it is off. Starting clean makes it answer "is
invoking rtk by hand actually worth it?" instead.
rtk's own config is chezmoi-managed (.chezmoitemplates/rtk-config.toml, one
template → ~/.config/rtk/ on linux, ~/Library/Application Support/rtk/ on
darwin). It pins telemetry off and carries no [hooks] section: with the
allowlist deciding what reaches rtk, a denylist there is unreachable policy and
a second place to edit.
Four formatters, one file type each, all installed via mise and all usable from
Emacs (apheleia) or the shell. oxfmt is repo-local (.mise.toml); the rest
are in the fleet baseline:
| Type | Tool | Config |
|---|---|---|
| TOML | taplo |
.taplo.toml |
| shell | shfmt |
.editorconfig |
| Markdown | oxfmt |
(defaults) |
| TS/JSON | prettier |
(defaults) |
taplo fmt --check # TOML
shfmt -d install.sh shell/*.sh private_bin/* # shell (skips the python ones)
oxfmt --check **/*.md # MarkdownTwo decisions worth knowing:
align_entries = truein.taplo.toml. taplo's default collapses the hand-aligned=columns infresh.toml; this keeps them. The cost is that taplo column-aligns trailing# whycomments rather than leaving them at a fixed two spaces..editorconfigrecords two indent dialects —private_bin/*uses tabs,install.shandshell/*.shuse four spaces — rather than unifying them. shfmt reads it, so a bareshfmt -drespects both.
oxfmt handles Markdown rather than prettier. It delegates .md to prettier
internally, so output is near-identical (verified byte-for-byte on this repo's
files, bar one case where oxfmt correctly leaves * unescaped inside a table
cell). It is one binary for the job and much faster. Note that prettier's
Markdown defaults normalize *emphasis* to _emphasis_ and re-pad every table
— that reflow is the bulk of any Markdown diff.
echo $SHELL # /opt/homebrew/bin/zsh
which brew # /opt/homebrew/bin/brew
starship --version # Prompt
zoxide --version # Directory jumping
mise doctor # Toolchain healthy
chezmoi status # Empty = $HOME matches the repo