Dev Setup

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

Tom Harada February 2026 ~10 min read
Contents
  1. Hardware
  2. macOS Configuration
  3. Terminal & Shell
  4. Dotfiles Deep Dive
  5. Coding Agents & Pi
  6. 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

I use Ghostty as my terminal emulator with tmux running inside it for session management and pane splitting. The shell is zsh with vi-mode keybindings.

Ghostty Config

~/.config/ghostty/config
window-decoration = true
shell-integration-features = no-title
title = " "
command = /opt/homebrew/bin/tmux new-session

Auto-launch tmux. Ghostty launches directly into a tmux session — no extra step. The title = " " keeps the title bar clean since tmux manages its own status. Shell integration title is disabled to avoid conflicts with tmux's pane labels.

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.

.tmux.conf

Tmux is the session manager that ties everything together. This config focuses on seamless pane navigation and a minimal status bar.

~/.tmux.conf
# Mouse support (scroll, click panes, resize)
set -g mouse on

# Generous scroll history
set -g history-limit 500000

# Active pane border - subtle gold highlight with top status
set -g pane-border-style 'fg=colour250'
set -g pane-active-border-style 'fg=colour136'
set -g pane-border-status top
set -g pane-border-format ' #{pane_index}: #{pane_current_command} '

# ── Pane navigation / auto-split ──
# Ctrl-hjkl: move to adjacent pane, or SPLIT a new one at the edge
bind -n C-h if -F '#{pane_at_left}'   'split-window -hb ...' 'select-pane -L'
bind -n C-j if -F '#{pane_at_bottom}' 'split-window -v  ...' 'select-pane -D'
bind -n C-k if -F '#{pane_at_top}'    'split-window -vb ...' 'select-pane -U'
bind -n C-l if -F '#{pane_at_right}'  'split-window -h  ...' 'select-pane -R'

# ── Pane management ──
bind -n C-x confirm-before -p "Kill pane? (y/n)" kill-pane
bind -n C-z resize-pane -Z              # Toggle zoom
bind -n C-= select-layout even-horizontal

# Transparent status bar (inherits terminal background)
set -g status-style 'fg=default,bg=default'
set -g window-status-current-style 'fg=colour136,bg=default,bold'

# Passthrough for image protocols, true color
set -g allow-passthrough on
set -g default-terminal "tmux-256color"
set -ga terminal-overrides ',xterm-ghostty:Tc'

Ctrl-HJKL with auto-split is the killer feature. Press Ctrl-H/J/K/L to navigate between panes — but if you're already at the edge, it automatically creates a new pane in that direction. No need to remember separate split and navigate keybindings. Want a new pane to the right? Just Ctrl-L when you're on the rightmost pane. It even inherits your current working directory.

500K scroll history ensures you never lose output from long-running builds. The transparent status bar means tmux visually disappears into Ghostty — no jarring green bar. The gold highlight on the active pane border is subtle but enough to always know where you are.

.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 tmux and shell configs. 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 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.

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 p (pi with bedrock) 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
  p "${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" | p "${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 p alias (defined elsewhere) points to the Bedrock-configured Pi binary. This keeps the cloud provider config in one place.

Pi Setup

The fork at p10q/pi-mono wraps the upstream Pi with a Bedrock provider integration, along with several custom agents and skills.

Key environment variable in .zshrc:

~/.zshrc
export PI_SPAWN_CMD=~/bin/p

Agent spawning. PI_SPAWN_CMD tells Pi which binary to use when spawning subagents. By pointing it at the Bedrock-wrapped p script, all spawned agents — including the ones below — automatically inherit the Bedrock provider config.

Custom Agents

I've built a few specialized agents that extend Pi's capabilities:

Composable agent architecture. The beauty of Pi's agent system is that each of these is just a configuration file — a system prompt, a set of tools, and optional skills. They compose together: the tmux-spawn agent can invoke the chrome-devtools agent in a separate pane, which can reference the figma-mcp agent for design specs. It's agents all the way down.

Combined Dotfiles

Complete, copy-ready versions of every config file discussed above. Copy to clipboard or tap to view the full file.

.zshrc
~/.zshrc · 56 lines
.tmux.conf
~/.tmux.conf · 33 lines
.vimrc
~/.vimrc · 50 lines
ghostty config
~/.config/ghostty/config · 4 lines
pc
~/bin/pc · 36 lines