An opinionated dotfiles setup designed to:
- work on Mac OS X with brew
- work on Ubuntu (particularly in dev containers and for GitHub Actions)
- be extensible to add other package managers
- I work on Mac OS X
- I'm trying to move to use dev containers
- I'm a DevOps Engineer by recent training, so like idempotent code
I looked at nix flakes but although I'm often tweaking my configuration, I don't need to set up whole new machines enough to warrant it. This blog from Julia Evans convinced me away from it.
For a brand new Mac, use the bootstrap script:
# Create directory structure and clone
mkdir -p ~/Developer/personal
cd ~/Developer/personal
git clone https://github.com/nickromney/n-dotfiles.git
cd n-dotfiles
# Run bootstrap to install essential tools
./bootstrap.sh
# Preferred installation flow
make install # Config-driven: brew, apt fallback, then mise runtimes
make personal stow # Stow configurations
make personal configure # Apply macOS settingsIf you already have Homebrew and basic tools:
# Clone and enter directory
git clone https://github.com/nickromney/n-dotfiles.git ~/n-dotfiles
cd ~/n-dotfiles
# Install the default safe/base toolchain (common profile)
make install
# Install personal additions
make install personal
# Provision a work Mac
make work install
# Apply macOS tweaks (dock, defaults) for the active profile
make personal configure
# Or just install VSCode and extensions
make focus-vscodeThe Makefile provides convenient targets for different configurations:
Combine a profile (common, personal, work, or all) with an action (install, update, stow, configure). Order does not matter, so make work install equals make install work.
# Profile + action examples
make install # Safe base install (common profile default)
make install personal # Install personal apps and CLIs
make work update # Update tooling for work machines
make stow work # Symlink configs for the work profile
make personal configure # Apply macOS defaults (dock, keyboard, etc.)
make install all # Install all profile bundles
make install PROFILE=work # Alternate syntax using PROFILE env var
make app-store install # Optional Mac App Store apps (after "purchasing" once)
# Focus targets (specific tool categories)
make vscode # VSCode and extensions
make neovim # Neovim and plugins
make app-store # Mac App Store apps (requires App Store login)
# VSCode for different editors
VSCODE_CLI=cursor make vscode # Install extensions for Cursor
# Package manager updates (updates both the manager and all installed packages)
make brew update # Update Homebrew and all brew/cask packages
make cargo update # Update Rust toolchain and cargo binaries
make uv update # Update uv package manager (use ./install.sh -u for uv tools)
make mas update # Update Mac App Store applications
> **Note:** Mac App Store installs require you to sign in via the App Store app and click "Get" once per app before `make app-store install` (or any `mas install`) can succeed.
> **Note:** Some tools (`arkade`, `slicer`) install their binaries to `/usr/local/bin` which is
> root-owned on Apple Silicon Macs. Self-update commands for these tools (`arkade update`,
> `slicer update`) require `sudo` as a result. This is expected — the install scripts for both
> tools write to `/usr/local/bin` by default, and the root ownership is not a Homebrew concern
> on Apple Silicon (Homebrew uses `/opt/homebrew` instead).- Automatically detects available package managers
- Skips unavailable package managers without failing
- Installs only tools that match available package managers
- Uses GNU Stow for configuration management
- Force mode (
-f) to handle existing configurations
# Preferred workflow
make install # safe base/common profile
make install personal # personal additions (or: make install work)
make app-store install # optional, run after clicking "Get" in App Store
make stow personal # symlink dotfiles
make personal configure # apply macOS settings
# Direct install.sh usage (legacy/CI)
./install.sh # Install packages only
./install.sh -s # Install packages and stow configurations
./install.sh -d -s # Preview changes
./install.sh -s -f # Force stow
./install.sh -u # Update installed packagesmake install follows this order:
- Read selected
_configs/*.yamlbundle(s) - Generate plain-text manifests (
Brewfile,arkade.tsv,metadata.json) - Apply system-wide dependencies with native tools (
brew bundle,arkade, manager-specific runners) - Fall back to
aptforbrewpackage tools whenbrewis unavailable - Install project runtimes via
mise installfrom localmise.toml
# One-shot preferred install path
make install
# Optional helpers
make install-system # system deps only (config-driven install.sh)
make runtime-install # project runtimes only (mise.toml)
make install-dry-run # preview system + runtime changes
make manifests-generate # inspect generated install manifests for the selected profileLight-touch macOS configuration management:
# Show current system settings
./_macos/macos.sh
# Apply personal configuration
./_macos/macos.sh personal.yaml
# Dry run to preview changes
./_macos/macos.sh -d personal.yaml
# Equivalent Makefile helper
make personal configureSee _macos/README.md for detailed macOS configuration options.
This repository includes comprehensive 1Password integration for secure credential and configuration management.
The setup-ssh-from-1password.sh script manages SSH configuration with security by default:
# Download base SSH config + per-profile fragment + public keys only
./setup-ssh-from-1password.sh
# Check what's available without downloading
./setup-ssh-from-1password.sh --dry-runIn safe mode:
- Downloads base SSH config from 1Password (stored as Secure Note)
- Downloads a per-profile SSH config fragment from 1Password
- Downloads public keys only for reference
- Private keys remain in 1Password
- Uses 1Password SSH Agent for authentication
# Download private keys (requires explicit confirmation)
./setup-ssh-from-1password.sh --unsafeUse unsafe mode when:
- 1Password SSH Agent cannot be installed in your environment
- You're using a restricted system without agent support
- You need keys for backup/migration purposes
The setup-gitconfig-from-1password.sh script manages work-specific Git configurations:
# Download work Git config from 1Password
./setup-gitconfig-from-1password.sh
# Check availability without downloading
./setup-gitconfig-from-1password.sh --dry-runThis allows you to:
- Store work-specific Git config in 1Password
- Automatically apply it to
~/Developer/work/.gitconfig_include - Keep work email and GitHub Enterprise settings secure
- Use
includeIfin main.gitconfigfor automatic switching
The aws/.aws/aws-1password script provides on-demand AWS credential fetching:
# Configure AWS CLI to use 1Password
aws configure set credential_process "$HOME/.aws/aws-1password --username default"
# For different profiles
aws configure set credential_process "$HOME/.aws/aws-1password --username tfcli" --profile terraformThis approach:
- Never stores AWS credentials on disk
- Fetches credentials from 1Password when needed
- Works seamlessly with AWS CLI and SDKs
- Supports multiple AWS accounts/profiles
- Open 1Password and create new item → SSH Key
- Name it exactly as expected by the script:
personal_github_authenticationpersonal_github_signingwork_2024_client_1_awswork_2025_client_1_githubwork_2025_client_2_githubwork_2025_client_2_giteawork_2025_client_2_ado
- Paste your private key
- Save to the vault expected by the script for that key
-
Create new item → Secure Note
-
Name it:
~/.ssh/config -
Add your base SSH configuration, for example:
Host * IdentityAgent "~/.1password/agent.sock" Include ~/.ssh/config.d/*.conf Include ~/.ssh/config.d/*/*.conf
-
Save it in the vault selected by
SSH_CONFIG_VAULTorVAULT
- Create new item → Secure Note
- Name it as one of:
~/.ssh/config.d/personal.conf~/.ssh/config.d/work-2024-client-1.conf~/.ssh/config.d/work-2025-client-1.conf~/.ssh/config.d/work-2025-client-2.conf
- Add only the host stanzas for that profile
- Save it in the same vault as that profile's SSH keys
-
Create new item → Secure Note
-
Name it:
work .gitconfig_include -
Add your work-specific Git configuration:
[url "github-work:OrgName/"] insteadOf = [email protected]:OrgName/ insteadOf = https://github.com/OrgName/ [url "git@ado-work-2025-client-2:v3/ORG/PROJECT/"] insteadOf = [email protected]:v3/ORG/PROJECT/ [user] email = [email protected]
-
Save to "Private" vault
- Create new item → API Credential (or custom item)
- Name it based on your mapping (e.g.,
AWSCredsUsernameDefault) - Add fields:
ACCESS_KEY: Your AWS Access Key IDSECRET_KEY: Your AWS Secret Access Key
- Save to "CLI" vault (or adjust in script)
- No secrets in version control: All sensitive data stays in 1Password
- Encrypted at rest: 1Password handles all encryption
- Audit trail: 1Password logs all access to credentials
- Easy rotation: Update credentials in one place
- Team sharing: Safely share vaults with team members
- MFA protection: Additional security with 1Password's MFA
The dotfiles manage PATH configuration for Rust/Cargo in zsh/.zshrc (lines 197-208). To prevent rustup from modifying your shell files during installation:
# Install Rust without modifying shell configuration
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --no-modify-pathThe --no-modify-path flag prevents rustup from adding its own PATH configuration to your shell files, since the dotfiles already handle $HOME/.cargo/bin in the managed PATH configuration.
Alternatively, you can:
- Set
RUSTUP_MODIFY_PATH=falsebefore running the installer - Choose "Customize installation" (option 2) and decline PATH modification
The ZSH configuration automatically adds tool directories to PATH if they exist:
$HOME/.local/bin- Local user binaries$HOME/.cargo/bin- Rust/Cargo binaries$HOME/.lmstudio/bin- LM Studio CLI$HOME/.tfenv/bin- Terraform version manager
Each directory is only added if it exists, preventing errors on partial installations.
The ZSH configuration (in zsh/.zshrc) uses defensive programming - every tool is checked before use, ensuring it works across all environments (personal Mac, work Mac, fresh installs, dev containers).
Quick file finding with syntax-highlighted previews:
f # Launch fzf fuzzy finder
bf # Select file, open in bat pager
nf # Select file, open in neovim
pf # Select file, copy path to clipboardAll commands show bat-powered syntax highlighting with line numbers in the preview pane.
The shell adapts based on installed tools:
- Completions: kubectl, gh, and other CLI tools
- Integrations: direnv, zoxide, starship, mise
- Plugins: zsh-autosuggestions, zsh-syntax-highlighting (via Homebrew)
- Aliases: Conditional git, navigation, and file listing shortcuts
See zsh/README.md for complete shell configuration documentation.
The _configs/ directory uses a layered approach:
_configs/
├── shared/ # Cross-platform tools
│ ├── shell.yaml # Shell utilities (zsh, starship, etc.)
│ ├── git.yaml # Git tools
│ ├── search.yaml # Search tools (ripgrep, fzf, etc.)
│ ├── neovim.yaml # Neovim configuration
│ ├── file-tools.yaml # File management utilities
│ ├── data-tools.yaml # Data processing tools
│ └── network.yaml # Network utilities
├── host/ # Host-specific configurations
│ ├── common.yaml # Tools for any Mac (Ghostty, VSCode, Obsidian, etc.)
│ ├── personal.yaml # Personal additions
│ └── work.yaml # Work-specific tools
└── focus/ # Development focus areas
├── vscode.yaml # VSCode + 38 extensions
├── python.yaml # Python development
├── typescript.yaml # TypeScript/Node development
├── rust.yaml # Rust development
├── kubernetes.yaml # Kubernetes tools
└── container-base.yaml # Base container tools
## All Available Configurations
### Tool Configurations (_configs/)
| Configuration | Type | Description | Use Case |
|--------------|------|-------------|----------|
| **focus/ai** | Focus | AI/ML tools (ollama, etc.) | AI development |
| **focus/container-base** | Focus | Podman and container tools | Container development |
| **focus/kubernetes** | Focus | K8s tools (kubectl, k9s, helm) | Kubernetes management |
| **focus/neovim** | Focus | Extended Neovim plugins | Advanced vim setup |
| **focus/python** | Focus | Python dev tools (uv, ruff) | Python development with Rust-based tooling |
| **focus/rust** | Focus | Rust toolchain and utilities | Rust development |
| **focus/typescript** | Focus | Node.js, TypeScript, Biome | JavaScript/TypeScript dev with Rust-based linting |
| **focus/vscode** | Focus | VSCode + 38 extensions | Full VSCode development |
| **host/common** | Host | Common Mac apps (Ghostty, VSCode, Obsidian) | Standard Mac productivity |
| **host/personal** | Host | Personal additions (games, media apps) | Personal Mac extras |
| **host/work** | Host | Work-specific tools | Work Mac requirements |
| **shared/data-tools** | Shared | Data processing (jq, yq, csvlens) | JSON/YAML/CSV manipulation |
| **shared/file-tools** | Shared | File management (eza, tree, etc.) | Directory navigation |
| **shared/git** | Shared | Git tools (delta, lazygit, gh CLI) | Version control essentials |
| **shared/neovim** | Shared | Neovim and plugins | Text editor setup |
| **shared/network** | Shared | Network utilities (httpie, curlie, etc.) | API testing and network debugging |
| **shared/search** | Shared | Search tools (ripgrep, fzf, fd, etc.) | File and text searching |
| **shared/shell** | Shared | Shell utilities (zsh, starship, atuin, etc.) | Essential for all setups |
### macOS System Settings (_macos/)
| Configuration | Description | Key Settings |
|--------------|-------------|--------------|
| **personal.yaml** | Personal Mac settings | Natural scroll, dock apps, keyboard shortcuts |
| **work.yaml** | Work Mac settings | Corporate defaults, security settings |
### Makefile Targets (Convenient Combinations)
| Target | Includes | Purpose |
|--------|----------|---------|
| **make install** | All shared/ + host/common | Safe base install (default profile) |
| **make common install** | All shared/ + host/common | Essential Mac setup |
| **make ai** | focus/ai | AI/ML development tools |
| **make app-store** | focus/app-store | Optional Mac App Store apps (requires prior purchase) |
| **make container-base** | focus/container-base | Podman and container tools |
| **make containers** | focus/containers | Podman container tools |
| **make hardware-home** | focus/hardware-home | Optional home hardware + chargeable apps |
| **make kubernetes** | focus/kubernetes | Kubernetes toolchain |
| **make neovim** | focus/neovim | Enhanced Neovim |
| **make python** | focus/python | Python development |
| **make rust** | focus/rust | Rust development |
| **make typescript** | focus/typescript | TypeScript/Node.js |
| **make vscode** | focus/vscode | VSCode + extensions |
| **make personal install** | Shared + host/common + host/personal + focus/containers + focus/kubernetes + focus/vscode + focus/cloud + focus/ai + focus/typescript | Base personal Mac (no hardware/chargeable extras) |
| **make work install** | Shared + host/common + host/work + focus/containers + focus/kubernetes + focus/vscode | Work laptop tooling |
| **make all install** | Shared + host/common + host/personal + host/work + focus/containers + focus/kubernetes + focus/vscode + focus/hardware-home + host/manual-check | Full superset |
| **make install hardware-home** | focus/hardware-home | Optional home hardware and chargeable apps |
| **make work-setup** | Runs setup-work-mac.sh | Legacy scripted work setup |
### Quick Setup Guide
For a new personal Mac (like yours):
```bash
# 1. Install base tools and personal apps
make personal install
# 2. Optional: install Mac App Store apps once you're signed in + clicked "Get"
make app-store install
# 3. Create configuration symlinks
make personal stow
# 4. Apply macOS system settings (mouse scroll, dock, etc.)
make personal configure
# Or stow + configure together:
make personal stow && make personal configure
noahgorstein/tap:
manager: brew
type: tap
check_command: "brew tap | grep -q '^noahgorstein/tap$'"
install_args: []kitty:
manager: brew
type: cask
check_command: 'brew list --cask kitty >/dev/null 2>&1 || [ -d "/Applications/kitty.app" ] || which kitty >/dev/null 2>&1'
install_args: []bat:
manager: brew
type: package
check_command: "bat --version"
install_args: []posting:
manager: uv
type: tool
check_command: "which posting >/dev/null 2>&1"
install_args: ["--python", "3.12"]prettier-vscode:
manager: code
type: extension
extension_id: esbenp.prettier-vscode
check_command: "code --list-extensions | grep -q esbenp.prettier-vscode"
description: "Code formatter"
documentation_url: "https://prettier.io/"
category: vscode-extensionEach tool entry requires:
manager: Package manager to use (brew/uv/cargo/apt/code/manual/mas)
type: Installation method specific to the manager
check_command: Command to verify installation
install_args: Additional installation arguments (optional)
skip_update: Set to true to opt the tool out of `install.sh -u` updates (optional)
extension_id: Required for VSCode extensions (manager: code)The code package manager supports VSCode and compatible editors:
# Default: uses 'code' CLI
make focus-vscode
# For Cursor editor
VSCODE_CLI=cursor make focus-vscode
# For VSCodium
VSCODE_CLI=vscodium make focus-vscode.
├── Brewfile # Preferred personal bundle (generated from _configs)
├── Brewfile.* # Profile/OS variants (work, common, posix)
├── install.sh # Thin entrypoint for the manifest-driven installer
├── Makefile # Convenient targets for common operations
├── _configs/ # Modular configuration files
│ ├── shared/ # Cross-platform tools
│ ├── host/ # Host-specific configurations
│ └── focus/ # Development focus areas
├── _macos/ # macOS system configuration
│ ├── macos.sh # macOS settings script
│ └── *.yaml # Settings profiles
├── _test/ # Comprehensive test suite
│ ├── install.bats # Installation tests
│ ├── macos.bats # macOS tests
│ ├── makefile.bats # Makefile tests
│ └── run_tests.sh # Test runner
└── */ # Stow directories for dotfiles
├── aerospace/ # Tiling window manager
├── git/ # Git configuration
├── nvim/ # Neovim config
├── tmux/ # Tmux config
├── vscode/ # VSCode settings
├── zsh/ # Zsh configuration
└── ... # Other tool configsThe repository includes a comprehensive test suite using BATS (Bash Automated Testing System):
# Install BATS (required for testing)
brew install bats-core # macOS
sudo apt-get install bats # Ubuntu/Debian
# Run all tests
./_test/run_tests.sh
# Run tests with specific filter
cd _test && bats install.bats --filter "install_tool"This repository now includes a Lima-based Ubuntu 24.04 smoke test for non-mac flows. It validates Linux Homebrew setup and runs the POSIX install/test path without applying macOS settings.
By default it uses the composed non-mac personal superset bundle at:
_configs/host/personal-posix.list
# Start/create test VM
make test-lima-up
# Run POSIX/non-mac smoke tests in the VM
make test-lima-run
# One-shot: start VM (if needed) + run smoke tests
make test-lima
# Optional lifecycle helpers
make test-lima-status
make test-lima-down
make test-lima-destroy
# Optional: make shellcheck failures blocking for this VM run
STRICT_SHELLCHECK=true make test-lima-run
# Optional: override the config set for a run
POSIX_CONFIG_FILES="shared/shell shared/git focus/kubernetes" make test-lima-runThe test suite includes:
- Unit tests for all utility functions
- Integration tests for package manager detection
- Installation tests for each package manager type
- macOS configuration tests with defaults mocking
- Mocking framework to simulate external commands
- 65+ comprehensive tests covering all major functionality
The install.sh script uses set -euo pipefail for strict error handling, which is a best practice for production scripts. However, this can cause issues in certain scenarios:
-
Testing: When functions are sourced in test environments, commands that normally fail (like checking for non-existent commands) will cause the entire function to exit.
-
Information Gathering: Functions that check system state need to handle failures gracefully without exiting.
The script handles this by temporarily disabling errexit in functions that need to tolerate failures:
get_available_managers() {
# Save current errexit setting and disable it
local old_errexit
old_errexit=$(set +o | grep errexit)
set +e
# ... function body that may have failing commands ...
# Restore errexit setting before returning
eval "$old_errexit"
return 0
}This pattern ensures:
- The function can complete even if some commands fail
- The original shell options are preserved
- The script maintains strict error handling elsewhere
- idcrook - elegant usage of GNU stow
- Typecraft Dev - because of the excellent YouTube video walkthroughs - "be a better nerd"
- Omer Hamerman / DevOpsToolbox - again, a fan of the YouTube video walkthroughs
- Christian Sutter - I used to work with Christian, and learned lots from pair programming with him.
- Rob / Tech Craft - Not posted for a while, but excellent videos