Hardware
My hardware philosophy is simple: invest in things you touch all day, every day. A great keyboard, a sharp monitor, and a machine with enough headroom that you never wait for it.
Keyboard Mod
I use the Nulea split ergonomic keyboard, but with a twist: I rip out the numpad keys and velcro-attach an Apple Magic Trackpad in their place. This keeps the trackpad centered between the split halves, right where your thumbs naturally rest. No more reaching for a mouse — and the ergonomic split keeps your wrists happy during long sessions.
Computer
If you use your laptop as your primary machine — especially for mobile development, client builds, or running multiple dev environments — max out the specs. The M4 Max with 128GB RAM and 8TB storage handles everything I throw at it without breaking a sweat. I also keep a cloud desktop (EC2) for heavier workloads and remote pairing.
Headphones
I use Apple wired USB-C EarPods. I lost too many AirPods Pro to the laundry. These work fine — good microphone, easy to switch between phone and laptop, and they just work. Sometimes the simplest tool is the best tool.
macOS Configuration
A few small macOS tweaks that make a huge difference for keyboard-heavy workflows.
System Preferences
- Caps Lock → Control — The single most impactful remap. Ctrl is used constantly in terminals and Vim, and Caps Lock is in the perfect position for it. System Preferences → Keyboard → Modifier Keys.
- Key Repeat: fastest / Delay: shortest — Makes navigating text feel instant. Holding down
jin Vim should feel like flying.
defaults write
These commands disable the press-and-hold character picker in favor of key repeat — essential for Vim-style editing in VS Code and across macOS.
defaults write com.microsoft.VSCode ApplePressAndHoldEnabled -bool false defaults write -g ApplePressAndHoldEnabled -bool false defaults write NSGlobalDomain ApplePressAndHoldEnabled -bool false
Why three commands? The first targets VS Code specifically, the second sets the global default for the current user, and the third writes to NSGlobalDomain as a catch-all. Belt and suspenders — some apps read one, some read another.
Chrome Extensions
- ARIA DevTools — Inspect accessibility tree directly in the browser. Essential for building accessible UIs.
- Tampermonkey — Run custom userscripts on any page. Useful for tweaking internal tools, adding shortcuts, or automating repetitive web tasks.
Terminal & Shell
The terminal is cmux — a full terminal app built on top of libghostty. Unlike a plain terminal emulator, cmux has a workspace sidebar (like Cursor's agent view), a built-in browser, split panes, and session management. It calls into libghostty under the hood for rendering, so the Ghostty config below applies. The shell is zsh with vi-mode keybindings.
Ghostty config (used by cmux)
cmux uses libghostty for rendering, so it reads the standard Ghostty config. The config is intentionally minimal — the main thing worth documenting is the color scheme: Solarized Light.
# Solarized Light Color Scheme background = #fdf6e3 foreground = #073642 # Normal colors palette = 0=#073642 palette = 1=#dc322f palette = 2=#859900 palette = 3=#b58900 palette = 4=#268bd2 palette = 5=#d33682 palette = 6=#2aa198 palette = 7=#eee8d5 # Bright colors palette = 8=#002b36 palette = 9=#cb4b16 palette = 10=#586e75 palette = 11=#657b83 palette = 12=#839496 palette = 13=#6c71c4 palette = 14=#93a1a1 palette = 15=#fdf6e3 # Cursor & selection cursor-color = #586e75 cursor-text = #fdf6e3 selection-background = #eee8d5 selection-foreground = #586e75
Why cmux? Most terminal workflows end up reinventing a workspace view — jumping between tmux sessions, keeping track of which pane is which, context-switching constantly. cmux makes that structure explicit: workspaces live in a sidebar, each with its own pane layout and built-in browser tab. It's particularly useful when running coding agents in parallel — each agent gets its own workspace rather than getting lost in a sea of tmux splits.
Why Solarized Light? Light themes are easier to read on high-brightness screens and in daylit environments. Solarized's warm off-white background (#fdf6e3) is softer than pure white, and the palette has enough contrast for syntax highlighting, git status colors, and shell prompts without being harsh.
libghostty as the backend means cmux inherits Ghostty's rendering quality — native GPU acceleration, true color, ligatures, and hyperlink support — without needing a separate Ghostty install running. The config file is shared between both if you use Ghostty standalone too.
Dotfiles Deep Dive
These are the configuration files I carry across machines. Each one is annotated below. Full backups live in a dedicated directory and are periodically snapshotted.
.zshrc
The shell configuration — aliases, history, vi-mode, path setup, and tool integrations. This is the longest file and the one that evolves most.
alias l='CLICOLOR_FORCE=1 ls -altrh' alias gloa='git log --all --oneline --graph --decorate' alias gloar='git log --all --oneline --graph --decorate --remotes' alias gs='git status' alias gfa='git fetch --all --prune'
Short aliases, big wins. l gives a colorized, reverse-time-sorted listing — the most recent files at the bottom where your cursor is. gloa is a compact graph view of all branches. gfa fetches everything and prunes stale remotes in one shot. These save hundreds of keystrokes per day.
HISTFILE=$HOME/.zsh_history HISTSIZE=100000 SAVEHIST=100000 setopt appendhistory # Append history to the history file (no overwriting) setopt sharehistory # Share history across terminals setopt incappendhistory # Write immediately, don't wait for shell exit
100K lines of history, shared everywhere. With sharehistory and incappendhistory, every terminal session can see commands from every other session in real time. Combined with Ctrl-R reverse search, this becomes a personal command database.
set -o vi bindkey -v # Vi-mode keybindings bindkey fd vi-cmd-mode # Escape to normal mode with 'fd' PROMPT='%n@%m %* %~ %# ' alias h1='history 1' alias vi='nvim' export VISUAL="vim" export EDITOR="$VISUAL"
Vi everywhere. The shell runs in vi-mode, and fd is mapped to Escape — the same mapping used in the .vimrc and nvim configs. This muscle memory carries across every tool. vi is aliased to nvim so the modern editor is always the default.
The prompt includes username, hostname, time, and full path — useful when you're SSH'd into remote machines and need context at a glance.
# Cache brew shellenv (static output, no need to fork every time)
export HOMEBREW_PREFIX="/opt/homebrew"
export HOMEBREW_CELLAR="/opt/homebrew/Cellar"
export HOMEBREW_REPOSITORY="/opt/homebrew"
export PATH="/opt/homebrew/bin:/opt/homebrew/sbin${PATH+:$PATH}"
# Hardcoded path (was: code --locate-shell-integration-path zsh — took 5s!)
[[ "$TERM_PROGRAM" == "vscode" ]] && . "/Applications/Visual Studio Code.app/..."
# Ruby / rbenv (lazy-loaded — only runs rbenv init when you first use ruby)
_rbenv_lazy_init() {
unfunction ruby gem irb bundle rake 2>/dev/null
eval "$(rbenv init - --no-rehash)"
}
for cmd in ruby gem irb bundle rake; do
eval "$cmd() { _rbenv_lazy_init; $cmd \"\$@\" }"
done
# fnm - Fast Node Manager (instead of nvm)
eval "$(fnm env --use-on-cd --shell zsh)"
# Starship prompt
eval "$(starship init zsh)"Shell startup speed matters. Several optimizations here: Homebrew's shellenv output is hardcoded instead of forked every launch. VS Code's shell integration path is hardcoded (the code --locate-... command took 5 seconds!). Ruby's rbenv is lazy-loaded — it only initializes when you first run ruby, gem, etc. And fnm replaces nvm as the Node version manager because it's dramatically faster.
.vimrc / nvim init.lua
The Vim config is kept minimal — no plugin manager, no 200-line statusline. Just keybindings and sensible defaults. The nvim init.lua is a Lua translation of the same config.
inoremap fd <Esc> vnoremap fd <Esc> " Swap ; and : — enter command mode without Shift nnoremap ; : nnoremap : ; vnoremap ; : vnoremap : ; " Split navigation with Ctrl-HJKL set splitbelow set splitright noremap <C-h> <C-w>h noremap <C-j> <C-w>j noremap <C-k> <C-w>k noremap <C-l> <C-w>l " Same in terminal mode tnoremap <C-h> <C-\><C-n><C-w>h tnoremap <C-j> <C-\><C-n><C-w>j tnoremap <C-k> <C-\><C-n><C-w>k tnoremap <C-l> <C-\><C-n><C-w>l
The same movement everywhere. fd for Escape, Ctrl-HJKL for pane navigation — identical to the shell config. The ;/: swap is a classic Vim optimization: you enter command mode with ; (no Shift key), and the rarely-used repeat-find moves to :. Over a day of editing, this saves thousands of Shift keypresses.
syntax on filetype plugin indent on set encoding=utf-8 set tabstop=4 set softtabstop=4 set shiftwidth=4 set expandtab " Spaces, not tabs set autoindent set fileformat=unix set mouse= " Disable Vim mouse — let the terminal handle drag/copy set incsearch " Search as you type set hlsearch " Highlight results set ignorecase " Case-insensitive search... set smartcase " ...unless uppercase used set backspace=indent,eol,start set clipboard=unnamed " System clipboard integration " Remove trailing whitespace on save (Python files) autocmd BufWritePre *.py :%s/\s\+$//e " Persistent undo, swap, and backup set undofile set undodir=~/.vim/undo// set swapfile set directory=~/.vim/swap// set backup set backupdir=~/.vim/backup// silent !mkdir -p ~/.vim/swap ~/.vim/backup ~/.vim/undo
Persistent undo is the unsung hero here. Close a file, reopen it days later, and you can still undo. The // suffix on directory paths tells Vim to use the full file path in the swap/backup name, avoiding collisions. Trailing whitespace is auto-stripped on Python files because PEP 8.
set mouse= (empty) disables Vim's built-in mouse handling entirely. The default mouse=a intercepts drag events, causing Vim to enter visual mode instead of letting the terminal select text. With mouse disabled, drag-to-select and Cmd+C / Cmd+V work exactly like they do outside of Vim.
nvim init.lua
The Neovim config is a Lua translation of the same vimrc settings. The key mouse setting maps directly: vim.opt.mouse = ''.
-- Key mappings
vim.keymap.set('i', 'fd', '<Esc>')
vim.keymap.set('v', 'fd', '<Esc>')
vim.keymap.set('n', ';', ':')
vim.keymap.set('n', ':', ';')
vim.keymap.set('v', ';', ':')
vim.keymap.set('v', ':', ';')
-- Basic settings
vim.cmd('syntax on')
vim.cmd('filetype plugin indent on')
vim.opt.encoding = 'utf-8'
vim.opt.mouse = '' -- Disable Neovim mouse handling; let the terminal handle drag/select/copy-paste
-- System clipboard integration
vim.opt.clipboard = 'unnamed'Why mouse = '' instead of 'a'? Neovim ships with mouse = 'a' as its default — mouse enabled in all modes. This means dragging enters visual selection mode inside Neovim, and you lose native terminal copy/paste. Setting it to empty string hands all mouse events back to the terminal so drag-to-select, Cmd+C, and middle-click paste all work as expected.
Coding Agents & Pi
I use Pi as my primary coding agent. I run a fork of pi-mono that adds a wrapper script for loading and using Pi with AWS Bedrock as the model provider.
The pc Script
I keep a pc (pi commit) script in ~/bin that runs Pi in non-interactive mode for quick tasks — especially auto-generating commit messages from staged changes.
#!/usr/bin/env bash
# pc: run pi in non-interactive mode
# Uses pi's -p (--print): process prompt and exit. No TUI.
# Usage: pc [--haiku|--micro|--lite] [prompt...]
# With no args (besides flags), generates a commit message from staged changes.
P_FLAGS=()
PROMPT_ARGS=()
while [[ $# -gt 0 ]]; do
case "$1" in
--haiku|--micro|--lite)
P_FLAGS+=("$1"); shift ;;
*)
PROMPT_ARGS+=("$1"); shift ;;
esac
done
if [[ ${#PROMPT_ARGS[@]} -ge 1 ]]; then
pi "${P_FLAGS[@]}" -p "${PROMPT_ARGS[*]}"
else
DIFF=$(git diff --cached)
if [[ -z "$DIFF" ]]; then
echo "No staged changes." >&2; exit 1
fi
PROMPT="Given the following git diff of staged changes, write a good
commit message using conventional commit format. Only output the
commit message, nothing else.
$DIFF"
MSG=$(echo "$PROMPT" | pi "${P_FLAGS[@]}" -p \
| perl -0pe 's/<thinking>.*?<\/thinking>\n?//gs')
if [[ -z "$MSG" ]]; then
echo "Failed to generate commit message." >&2; exit 1
fi
git commit -m "$MSG"
fiOne command to commit. pc with no arguments reads git diff --cached, sends it to Pi (backed by Bedrock), strips any thinking tags, and commits with the generated message. Pass --haiku or --lite for cheaper/faster models on trivial commits. Pass a prompt directly (pc "explain this codebase") and it works as a general-purpose CLI.
The pi alias (defined in .zshrc) wires up the AWS profile and points at the local pi-mono build. This keeps the provider config in one place — pc inherits it automatically.
Pi Setup
The fork at p10q/pi-mono wraps the upstream Pi with a Bedrock provider integration, along with several custom agents and skills.
Pi is invoked via a shell alias that wires up the AWS profile and points at the local build:
alias pi='AWS_PROFILE=<profile> node /path/to/pi-mono/packages/coding-agent/dist/cli.js -' alias pi-sonnet='pi --model us.anthropic.claude-sonnet-4-6' alias pi-opus='pi --model us.anthropic.claude-opus-4-6-v1' export PI_SPAWN_CMD="AWS_PROFILE=<profile> node /path/to/pi-mono/packages/coding-agent/dist/cli.js -"
The alias keeps provider config in one place. Prefixing with AWS_PROFILE means every Pi invocation — interactive or spawned — automatically uses the right Bedrock credentials without touching the global AWS config. Running pi in the terminal drops straight into an interactive session.
Model variants. pi-sonnet and pi-opus pin to specific model versions via --model. Reach for pi-sonnet when speed matters — fast iteration, quick edits, running through multiple ideas in sequence. Use pi-opus when a task genuinely benefits from deeper thinking: complex architecture decisions, tricky debugging, or anything where you'd rather it reason carefully once than go back and forth. Both inherit the same provider config from the base pi alias.
Agent spawning. PI_SPAWN_CMD mirrors the alias so that subagents spawned by Pi (or by cmux-spawn) inherit the same provider config. Both point at the same local build of pi-mono, so changes to agents and skills are picked up immediately without a reinstall.
Slash Commands
The ones I reach for most in any session:
/tree— Prints the repo's file tree into context. Most useful mid-session when something has gone sideways — it gives the agent a structural anchor to reorient from rather than trying to continue from a confused state./compact— Compresses the conversation history into a condensed summary while keeping the essential context. Useful in long sessions when the context window is getting heavy — it lets you keep going without starting over./sw(spawn workspace),/sr(spawn right),/sd(spawn down) — Spawn a Pi subagent directly into a new cmux workspace or split pane. The spawned agent inherits full context and runs in parallel./srand/sdkeep the new pane visible alongside the current one;/swopens it in a fresh workspace in the sidebar./spawns— Shows all active spawned sessions. From here you can jump to any of them, see what they're working on, or close ones that have finished. Keeps the parallel work visible rather than lost in background processes./export— Saves the current session to a file instantly. No delay, no summarization — just a snapshot of the full conversation. Useful before doing something risky, switching context, or handing a session off. Pick it back up later exactly where you left it.
/tree and /compact are complementary mid-session tools: /tree gives the agent a structural anchor when it's drifted, and /compact reclaims context budget so you can keep going without starting over.
/export is the save button. Instant, lossless. Use it before a risky change, before handing off to someone else, or just at a good stopping point. The session file is plain text — easy to search, share, or resume from.
The spawn commands are the real force multiplier. Being able to say "handle this subtask" and immediately have it running in a visible pane — not buried in a subprocess — changes how you think about agent workflows. You keep your main thread moving while parallel agents handle research, tests, or a separate feature branch. /spawns is the control surface for all of it.
Custom Agents
I use a few specialized agents that extend Pi's capabilities:
- cmux-pi-spawn — A Pi extension (analogous to cmux's built-in
cmux-spawn, which uses tmux) that integrates spawning natively with Pi. Liketmux-spawn, it ships as a Pi built-in. It's what powers the/sw,/sr,/sd, and/spawnsslash commands above — spawned agents open as real cmux workspaces and panes, not invisible background processes. Parallel agent work becomes something you can actually see and navigate. - chrome-devtools — Connects Pi to Chrome's DevTools Protocol for browser automation, visual testing, and interactive debugging. The agent can navigate pages, take screenshots, inspect the DOM, and execute JavaScript — all driven by natural language.
- figma-mcp — Integrates Figma's design data via MCP (Model Context Protocol). Lets Pi read design specs, extract colors and spacing tokens, and generate code that matches the design system. Bridges the gap between design files and implementation.
Composable agent architecture. The beauty of Pi's agent system is that each agent is just a configuration file — a system prompt, a set of tools, and optional skills. They compose together: cmux-pi-spawn can spin up a chrome-devtools agent in a separate pane, which can reference the figma-mcp agent for design specs. It's agents all the way down.
cmux-pi-spawn specifically closes the loop between Pi's parallel agent model and cmux's workspace UI. Before it, spawned agents were essentially invisible — you'd kick off a subtask and have no easy way to monitor or interact with it. Now each spawn is a first-class cmux workspace. The combination of /sw / /sr / /sd for spawning and /spawns for oversight makes parallel workflows practical rather than theoretical.
Combined Dotfiles
Complete, copy-ready versions of every config file discussed above. Copy to clipboard or tap to view the full file.