Dev Setup

A detailed walkthrough of my development environment — hardware, dotfiles, terminal configuration, coding agents. Everything annotated.

Tom Harada February 2026 ~16 min read
Contents
  1. Hardware
  2. macOS Configuration
  3. Terminal & Shell
  4. Ghostty config (used by cmux)
  5. Dotfiles Deep Dive
  6. Coding Agents & Pi
  7. Combined Dotfiles

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.

Wired split keyboard + Magic Trackpad mod
5K external monitor
MacBook Pro M4 Max
128GB / 8TB — max the specs

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

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.

Terminal
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

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.

~/Library/Application Support/com.mitchellh.ghostty/config
# 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.

~/.zshrc — Aliases & Basics
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.

~/.zshrc — History
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.

~/.zshrc — Vi Mode
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.

~/.zshrc — Performance Optimizations
# 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.

~/.vimrc — Core Keybindings
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.

~/.vimrc — Sensible Defaults
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 = ''.

~/.config/nvim/init.lua — Mouse & Key Settings
-- 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.

~/bin/pc
#!/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"
fi

One 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:

~/.zshrc
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 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:

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.

ghostty config
~/Library/Application Support/com.mitchellh.ghostty/config
.zshrc
~/.zshrc · 56 lines
.vimrc
~/.vimrc · 52 lines
init.lua
~/.config/nvim/init.lua
pc
~/bin/pc · 36 lines