Setup
Complete configuration guide for the Sao development environment.
System: Ubuntu 24.04.3 LTS (Noble) | User: sao
Table of Contents
- System Packages
- Programming Languages and Tools
- Neovim Configuration
- tmux Configuration
- Git Configuration
- Shell Configuration
- Remote Access
- Custom Keybindings
- File Locations
- Quick Start
System Packages
Core Development Tools
- build-essential (12.10ubuntu1): C/C++ compiler and build tools
- git (2.43.0-1ubuntu7.3): Version control
- curl (8.5.0-2ubuntu10.6): Data transfer tool (HTTP/HTTPS)
- wget (1.21.4): Data transfer tool (HTTP/HTTPS/FTP)
- tree (2.1.1): Directory lister
- tmux (3.4): Terminal multiplexer for session management
- ripgrep (14.1.0): Fast recursive search tool (required for Telescope live_grep)
- fzf: Command-line fuzzy finder
PHP
- php8.4-cli (8.4.22): PHP CLI runtime — from deb.sury.org PPA (Ubuntu 24.04 ships PHP 8.3)
- php8.4-pgsql (8.4.22): PostgreSQL PDO driver (
pdo_pgsql,pgsql) - php8.4-mysql (8.4.22): MySQL PDO driver (
pdo_mysql,mysqli,mysqlnd) — installed 2026-06-10
sudo apt-get install -y php8.4-mysql
Programming Languages and Tools
Go
- Version: go1.25.3 linux/amd64
- Installation Path:
/usr/local/go/bin/go - Added to PATH:
/usr/local/go/bin
NVM
- Installation:
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
Node.js (via NVM)
- Version: v18.20.8
- Installation:
nvm install 18 && nvm use 18 - NVM Directory:
~/.nvm - Node Path:
~/.nvm/versions/node/v18.20.8/bin/node
Claude CLI
- Path:
~/.nvm/versions/node/v18.20.8/bin/claude - Installation:
npm install -g @anthropic-ai/claude-code
Node.js Requirement
Claude CLI requires Node.js v18 or higher. Install via NVM before installing Claude CLI.
Settings (~/.claude/settings.json)
Synced from the shared gravity base (see Shell Configuration) — this VM carries no machine-local override.
| Setting | Value | Notes |
|---|---|---|
model |
opus[1m] |
Opus with the extended 1M-token context window |
permissions.defaultMode |
auto |
Skips per-action approval prompts |
editorMode |
vim |
New: vim-style input keybindings in the CLI |
enableAllProjectMcpServers |
true |
Auto-enables project-scoped MCP servers |
skipWorkflowUsageWarning |
true |
Newly upstreamed — previously a sao-only local setting |
Docker
- Docker Engine: 28.5.1
- Docker Compose: v2.40.0 (integrated plugin)
- User permissions:
saois in thedockergroup (can run without sudo)
Docker Compose Syntax
Use docker compose (with space) instead of the legacy docker-compose (with hyphen). The compose plugin is integrated into Docker Engine.
Neovim
- Version: NVIM v0.11.4 (Release)
- Installation Path:
/opt/nvim-linux-x86_64/bin - Config Location:
~/.config/nvim/ - Based on: kickstart.nvim (forked to github.com/Mike-Bros/kickstart.nvim)
- Added to PATH:
/opt/nvim-linux-x86_64/bin
Neovim Configuration
Plugin Manager
- lazy.nvim: Modern plugin manager with lazy loading
Installed Plugins
Core Capabilities (from kickstart.nvim)
The kickstart.nvim base provides:
- LSP & Language Support: Auto-configured language servers with Mason installer
- Code Completion: Smart completions with snippets and documentation
- Fuzzy Finding: Telescope for files, buffers, grep, and LSP symbols
- Syntax Highlighting: Treesitter for accurate code parsing
- Git Integration: Inline blame, diff views, and hunk operations
- UI Enhancements: Keybinding hints (which-key), progress notifications, Catppuccin colorscheme
- Editor Features: Auto-indent detection, surround operations, statusline, relative line numbers
Telescope Dependencies
Telescope's live_grep feature requires ripgrep to be installed on the system. It's included in the base tools installation above.
Custom Plugins (in ~/.config/nvim/lua/custom/plugins/)
- blade-nav.nvim:
gfnavigation for Blade components, views, routes, and config references - blink.compat: Bridges laravel.nvim's nvim-cmp completion source into blink.cmp
- catppuccin.nvim: Modern colorscheme with soothing pastel theme (replaces tokyonight)
- claudecode.nvim: AI coding assistant
- conform.nvim: Auto-format on save (goimports→gofumpt for Go, biome for TS/React, stylua for Lua)
- laravel.nvim: Laravel framework integration — Artisan, routes, and scaffolding pickers
- phpstorm-dx (local module,
lua/custom/phpstorm/): PhpStorm-style Ctrl+Click, right-click menu, and pinned project search - vim-be-good: Vim motion practice game
Claude Code Quick Reference
<space>ac- Toggle Claude Code terminalCtrl+w- Exit terminal mode and start window command- See Keybindings for complete reference
Language Servers & Tools (via Mason)
Installed in ~/.local/share/nvim/mason/bin/:
Go
- gopls: Go language server
- gofumpt: Go formatter
- goimports: Go import manager
TypeScript/JavaScript
- typescript-language-server (ts_ls)
- biome: Fast JS/TS/React formatter and linter
Lua
- lua-language-server (lua_ls)
- stylua: Lua formatter
PHP
- intelephense: PHP language server
- php-cs-fixer: PHP formatter (non-Laravel projects)
- blade-formatter: Laravel Blade template formatter
Laravel Projects Use Pint Instead
When a project has vendor/bin/pint (Laravel's own formatter, built on php-cs-fixer), conform.nvim prefers it over Mason's php-cs-fixer — see Formatter Configuration. Pint ships per-project via Composer, not Mason.
LSP Configuration
gopls (Go Language Server)
settings = {
gopls = {
gofumpt = true, -- Enable gofumpt formatting
staticcheck = true, -- Enable static analysis
completeUnimported = true, -- Suggest unimported packages
usePlaceholders = true, -- Function parameter placeholders
analyses = {
unusedparams = true,
shadow = true,
},
},
}
Formatting Configuration
Document formatting is disabled in gopls to avoid conflicts with conform.nvim
Formatter Configuration (conform.nvim)
Auto-Format on Save
All configured file types auto-format on save with a 5-second timeout.
Formatter chains (tries first formatter, falls back to second):
- Go:
goimports→gofumpt(import management + strict formatting) - JavaScript/TypeScript/React:
biome→prettier - JSON:
biome→prettier - PHP:
pint(project'svendor/bin/pint, Laravel only) →php_cs_fixer(fallback) - Blade:
blade-formatter - Lua:
stylua
Editor Appearance & Behavior
Color Scheme
- Catppuccin: Modern, soothing pastel theme
- Yellow line numbers: Custom highlight for improved visibility
- Location:
~/.config/nvim/lua/custom/plugins/catppuccin.lua
Custom Vim Options (in init.lua)
Line Numbers & Navigation
- Relative line numbers (
relativenumber): Shows relative distances from cursor for efficient vim motions (e.g.,5jto jump down 5 lines) - Custom line number color: Yellow highlighting for better visibility with Catppuccin theme
Indentation & Formatting
- 4-space indentation:
shiftwidth,tabstop, andsofttabstopall set to 4 - Spaces over tabs (
expandtab): Insert spaces when pressing Tab - Smart indenting (
smartindent): Automatically indent new lines based on syntax
Display & Scrolling
- No line wrapping (
wrap = false): Long lines scroll horizontally - Horizontal scroll offset (
sidescrolloff = 10): Keep 10 columns visible left/right of cursor when scrolling
Auto-Installation Note
Mason Auto-Installation
All language tooling (LSP servers, formatters, linters) listed above are automatically installed by Mason when you first open Neovim with this config. The host machine only needs:
- Go: Base language runtime
- Node.js: Base JavaScript runtime
Mason handles installing and managing:
- Language servers (gopls, ts_ls, lua_ls)
- Formatters (gofumpt, goimports, biome, stylua)
- All other development tools
No manual installation of language tooling required!
PhpStorm-Style Laravel DX
Two additions bring PhpStorm-like ergonomics to PHP/Laravel work: a standalone editing module and a Laravel-aware plugin stack.
custom.phpstorm (lua/custom/phpstorm/init.lua)
- Ctrl+Click: Go to definition — LSP-powered, opens a Telescope picker on multiple results, falls back to
gdif no LSP is attached - Right-click menu: Extended with Find Usages, Go to Implementation, and Rename Symbol (
mousemodel = popup_setpos) <C-f>: Project-wide "Search Everywhere" — normal mode reopens the last accepted query pinned; visual mode turns the selection into a literal query instantly. The last query persists across sessions (stdpath('state')/phpstorm_last_search)
Replaces Stock <C-f>
This remaps Vim's built-in page-down scroll. Use Ctrl-d/Ctrl-u (half-page scroll) instead.
See Keybindings for the full key table.
Laravel Stack (lua/custom/plugins/laravel.lua)
| Plugin | Purpose |
|---|---|
| laravel.nvim | Telescope pickers for Artisan commands, routes, make:* scaffolding, related files (model → migration/factory/policy), project resources, and command history |
| blade-nav.nvim | gf navigation through Blade components, included views, named routes, and config keys |
| blink.compat | Bridges laravel.nvim's nvim-cmp completion source into blink.cmp |
- Filetype:
*.blade.phpis registered as filetypeblade(treesitterblade+vueparsers) - Completion:
php/bladebuffers addlaravel(views, routes, config, env, model columns) andblade-navsources ahead oflsp/path/snippetsin blink.cmp - Loads on:
ft = { 'php', 'blade' }orBufEnter composer.json
See Keybindings for the full key table.
tmux Configuration
- Version: tmux 3.4
- Config Location:
~/.tmux.conf - Prefix Key:
Ctrl+a(instead of defaultCtrl+b) - Theme: Catppuccin Mocha
- Plugin Manager: TPM (Tmux Plugin Manager)
Core Features
Terminal & Display:
- True color support: RGB color enabled for Neovim and terminal applications
- Zero escape time: Eliminates delay when switching Neovim modes
- Mouse support: Click to select panes, windows, and scroll
- Large scrollback: 10,000 line history buffer
Session Management:
- Persistent sessions: Auto-saves every 1 minute, survives system reboots
- Auto-restore: Sessions automatically restored on tmux start
- Auto-renumber windows: Windows renumber when closed
Installed Plugins
TPM is installed at ~/.tmux/plugins/tpm and manages the following plugins:
- tmux-sensible: Sensible default settings
- tmux-yank: System clipboard integration
- vim-tmux-navigator: Seamless Vim/tmux pane navigation (works with Neovim)
- catppuccin/tmux: Catppuccin Mocha theme with custom status bar
- tmux-resurrect: Save and restore sessions manually
- tmux-continuum: Auto-save sessions every 1 minute
Plugin keybindings:
Ctrl+a I(capital I) - Install new pluginsCtrl+a U(capital U) - Update pluginsCtrl+a Alt+u- Uninstall plugins not in config
Git Status Integration
gitmux displays Git repository information in the tmux status bar:
- Location:
~/go/bin/gitmux(development version) - Config:
~/.gitmux.conf(Catppuccin color scheme) - Shows: Branch name, commits ahead/behind, staged/modified/untracked files
Status bar layout:
- Left: Session name
- Right: Current directory + Git status (when in git repo)
- Window indicator: Shows
(+)when window is zoomed/maximized
Windows Terminal Compatibility
The default tmux prefix (Ctrl+b) conflicts with Windows Terminal's tab switching when connecting via SSH. Using Ctrl+a avoids this conflict and provides a more ergonomic prefix key.
Configuration: See ~/.tmux.conf for full configuration details. For keybindings, see tmux Keybindings.
Git Configuration
Global Settings
user.name=Mike Bros
user.email=9326825+Mike-Bros@users.noreply.github.com
Authentication: SSH Keys
This system uses SSH key authentication for all Git operations with GitHub. SSH provides secure, password-free authentication without requiring credential storage.
Current Configuration:
- Private key:
~/.ssh/id_ed25519 - Public key:
~/.ssh/id_ed25519.pub(added to GitHub account) - Known hosts: GitHub added to
~/.ssh/known_hosts - Repository URLs: Use SSH format (
git@github.com:user/repo.git)
Setup Steps (for fresh installations):
-
Generate ED25519 SSH key pair:
ssh-keygen -t ed25519 -C "9326825+Mike-Bros@users.noreply.github.com" -f ~/.ssh/id_ed25519 -N "" -
Copy public key to clipboard:
Copy the entire output.cat ~/.ssh/id_ed25519.pub -
Add public key to GitHub:
- Visit GitHub Settings > SSH and GPG keys
- Click "New SSH key"
-
Paste the public key and save
-
Add GitHub to known hosts (prevents SSH fingerprint prompt):
ssh-keyscan github.com >> ~/.ssh/known_hosts -
Verify authentication:
Expected output:ssh -T git@github.comHi Mike-Bros! You've successfully authenticated, but GitHub does not provide shell access.
SSH Authentication Configured
This system is configured with SSH key authentication. No credential storage or personal access tokens are needed. All Git operations use the SSH key at ~/.ssh/id_ed25519.
SSH vs HTTPS URLs
Always clone repositories using SSH URLs (git@github.com:user/repo.git) instead of HTTPS URLs (https://github.com/user/repo.git). SSH authentication is more secure and doesn't require managing tokens or credential storage.
Shell Configuration
Both ~/.bashrc and ~/.zshrc are managed by gravity, a manifest-driven config sync system built into the Neovim config repo (~/.config/nvim). Each rc file layers a machine-local override (configs.overrides/) on top of a shared base (configs/); the base is version-tagged with the repo release (currently v1.34.1).
Shared Base
Both rc files carry the same tooling init:
| Tool | Purpose |
|---|---|
| NVM | Node.js version management ($NVM_DIR/nvm.sh) |
| mise | Runtime version manager — [ -x ~/.local/bin/mise ] && eval "$(mise activate <shell>)" |
| zoxide | Frecency-based cd replacement |
| starship | Shell prompt |
| uv | Python tooling — sources ~/.local/bin/env |
| Neovim | Adds /opt/nvim-linux-x86_64/bin to PATH; sets EDITOR/VISUAL |
~/.bashrc additionally sets export GOPRIVATE=git.bros.ninja, marking the internal Gitea host as a private Go module source (skips the public proxy/checksum DB).
Machine-Local Override: gh CLI Account Switch
This VM's only override on top of the shared base is _gh_account_switch, a hook (bash: PROMPT_COMMAND; zsh: chpwd) that swaps the gh CLI's config directory by working directory:
_gh_account_switch() {
case "$PWD/" in
"$HOME/stacks/sf-root/"*) export GH_CONFIG_DIR="$HOME/.config/gh-sf" ;;
*) unset GH_CONFIG_DIR ;;
esac
}
Under ~/stacks/sf-root, gh uses the ServiceFactor account (~/.config/gh-sf); everywhere else it falls back to the default personal account (~/.config/gh).
Drift Reconciled — 2026-08-06
A stale local override previously carried an rmc2login JWT helper and Google Cloud SDK sourcing. Neither was ever part of the shared base — both were dropped during reconciliation, leaving the override scoped to just the gh CLI switch.
Machine-Local Config Exclusions
Gravity's manifest (~/.config/nvim/manifest.json) declares every syncable config across all machines, including desktop-only entries (Hyprland, waybar, Omarchy) that don't apply to this headless VM. manifest.overrides.json (gitignored) now supports an exclude_configs list — exact manifest keys or * globs — to skip those on a per-machine basis:
{
"version": "1.0.0",
"exclude_configs": ["hypr-*", "waybar-*", "walker-*", "elephant-*", "omarchy-*", "wsnotes"]
}
Version Check Relaxed to Major-Only
manifest.overrides.json only needs to match the manifest's major version, so this gitignored per-machine file survives routine minor/patch releases without needing manual bumps.
Cleanup: 33 desktop-only config files that gravity had previously synced onto this headless VM — under ~/.config/hypr/, ~/.config/waybar/, ~/.local/share/omarchy/, plus loose scripts ~/.local/bin/omarchy-waybar-launch and ~/.local/bin/hypr-monitor-border-color — were deleted along with their now-empty directories.
Remote Access
SSH
- Server: openssh-server 1:9.6p1-3ubuntu13.16
- Service:
ssh.service— active and enabled (starts on boot) - Firewall: ufw inactive (no port-level restrictions)
- Reachable at:
sao@192.168.1.153(LAN) orsao@100.119.86.78(Tailscale)
Terminal Compatibility: Ghostty (xterm-ghostty)
Doubled/Overlapping Characters Over SSH
Ghostty sets TERM=xterm-ghostty and forwards it over SSH. Without a matching terminfo entry on the server, readline can't resolve the terminal's capabilities, causing doubled/overlapping characters while typing in SSH sessions.
Fix: Installed the canonical ghostty terminfo entry (from the ncurses invisible-island terminfo.src database) with xterm-ghostty added as an alias, compiled with tic -x for both the user and system:
tic -x -o ~/.terminfo ghostty.src # user: sao
sudo tic -x -o /usr/share/terminfo ghostty.src # system-wide
Both ~/.terminfo/x/xterm-ghostty and /usr/share/terminfo/x/xterm-ghostty are symlinks to ../g/ghostty.
Verify:
infocmp xterm-ghostty # resolves the entry
TERM=xterm-ghostty tput colors # → 256
Version-Exact Alternative
To match the client's exact Ghostty build instead of the ncurses database version, run this from the Ghostty client machine:
infocmp -x xterm-ghostty | ssh sao@<host> 'tic -x -'
Custom Keybindings
For a complete reference of all custom keybindings, see Keybindings.
File Locations
Configuration Files
- Neovim config:
~/.config/nvim/init.lua - Custom plugins:
~/.config/nvim/lua/custom/plugins/init.lua - Laravel/PHP stack:
~/.config/nvim/lua/custom/plugins/laravel.luaand~/.config/nvim/lua/custom/phpstorm/init.lua - tmux config:
~/.tmux.conf - gitmux config:
~/.gitmux.conf - Bashrc:
~/.bashrc(gravity base + local override in~/.config/nvim/configs.overrides/.bashrc) - Zshrc:
~/.zshrc(gravity base + local override in~/.config/nvim/configs.overrides/.zshrc) - Claude Code settings:
~/.claude/settings.json(gravity-synced, no local override) - Gravity machine overrides:
~/.config/nvim/manifest.overrides.json(gitignored) - Git config:
~/.gitconfig - SSH keys:
~/.ssh/id_ed25519(private) and~/.ssh/id_ed25519.pub(public) - NVM:
~/.nvm/
Data Directories
- Neovim plugins:
~/.local/share/nvim/lazy/ - Mason tools:
~/.local/share/nvim/mason/bin/ - tmux plugins:
~/.tmux/plugins/ - Go binaries:
~/go/bin/(includes gitmux) - Node modules:
~/.nvm/versions/node/v18.20.8/
Quick Start After Fresh Install
Prerequisites
These instructions assume a fresh Ubuntu 24.04 LTS installation. Adjust commands if using a different version or distribution.
To replicate this setup on a new Ubuntu installation:
-
Install base tools:
sudo apt update sudo apt install build-essential git curl wget tree tmux ripgrep fzf -
Install Go:
cd ~/Downloads wget https://dl.google.com/go/go1.25.3.linux-amd64.tar.gz sudo tar -C /usr/local -xzf go1.25.3.linux-amd64.tar.gz echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc source ~/.bashrc -
Install NVM:
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash # Reload shell or run: source ~/.bashrc -
Install Node.js via NVM:
nvm install 18 && nvm use 18 -
Install Neovim:
cd ~/Downloads wget https://github.com/neovim/neovim/releases/latest/download/nvim-linux-x86_64.tar.gz sudo tar -C /opt -xzf nvim-linux-x86_64.tar.gz echo 'export PATH="$PATH:/opt/nvim-linux-x86_64/bin"' >> ~/.bashrc echo 'export EDITOR=nvim' >> ~/.bashrc echo 'export VISUAL=nvim' >> ~/.bashrc echo 'alias vim=nvim' >> ~/.bashrc echo 'alias vi=nvim' >> ~/.bashrc source ~/.bashrc -
Install tmux and TPM:
sudo apt install tmux # Download the full tmux config curl -o ~/.tmux.conf https://raw.githubusercontent.com/Mike-Bros/kickstart.nvim/main/.tmux.conf # Install TPM (Tmux Plugin Manager) git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm # Install gitmux for git status in status bar go install github.com/arl/gitmux@latest # Download gitmux config curl -o ~/.gitmux.conf https://raw.githubusercontent.com/Mike-Bros/kickstart.nvim/main/.gitmux.conf # Start tmux and install plugins tmux # Press Ctrl+a then Shift+I to install plugins -
Install Claude CLI:
npm install -g @anthropic-ai/claude-code -
Clone kickstart.nvim config:
git clone https://github.com/Mike-Bros/kickstart.nvim.git ~/.config/nvim -
Open Neovim (auto-installs plugins, LSP servers, linters, and formatters):
nvim -
Configure Git and SSH:
# Basic Git configuration git config --global user.name "Mike Bros" git config --global user.email "9326825+Mike-Bros@users.noreply.github.com" # Generate SSH key for GitHub ssh-keygen -t ed25519 -C "9326825+Mike-Bros@users.noreply.github.com" -f ~/.ssh/id_ed25519 -N "" # Display public key to copy cat ~/.ssh/id_ed25519.pub # Copy the output and add it to GitHub Settings > SSH and GPG keys # Visit: https://github.com/settings/keys # Add GitHub to known hosts (prevents SSH fingerprint prompt) ssh-keyscan github.com >> ~/.ssh/known_hosts # Test SSH authentication ssh -T git@github.com # Expected: "Hi Mike-Bros! You've successfully authenticated..."