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 simple enough to reason about at a glance
- I work on Mac OS X
- I'm a DevOps Engineer by recent training, so like idempotent code
- The setup is three declarative layers, each owned by a tool someone else maintains:
| Layer | Owns | Source of truth |
|---|---|---|
brew bundle |
Casks, fonts, Mac App Store apps, mac-only formulae | Brewfile (Brewfile.posix on Linux) |
mise install |
Cross-platform CLI tools and language runtimes | mise/.config/mise/config.toml |
stow |
Dotfile symlinks into $HOME |
The stow trees (zsh/, git/, nvim/, ...) via stow.sh |
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.
AI CLIs (claude, codex, opencode, copilot) are deliberately unmanaged: they self-update via their own native installers, and pinning them through a package manager just fights the updater. Install them with their official one-liners and let them look after themselves.
Slicer and superterm are unmanaged for a different
reason: they're a paid tier, licensed together (currently billed through a
GitHub-linked subscription), not free packages a package manager should
track. Install and license them directly rather than through
Brewfile/mise — the shell configs (zsh/.zshrc, bash/.bashrc) already
guard their hooks with command -v superterm, so nothing breaks on a
machine without an active license.
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
# Preview, then run: Homebrew + Brewfile + stow + optional Borders + mise
./bootstrap.sh --dry-run --no-input --skip-1password
./bootstrap.sh
# Apply macOS settings (dock, defaults)
make configureOr run the whole personal flow (bootstrap + macOS settings + SSH from 1Password) in one script:
./setup-personal-mac.sh --dry-run
./setup-personal-mac.shmake install # host-appropriate package layer + stow + mise (idempotent)
make update # update brew, mise, and Mac App Store packagesIf an older unmanaged dotfile conflicts with a tracked Stow package,
make install moves the old file under
${XDG_STATE_HOME:-~/.local/state}/n-dotfiles/stow-backups/ before linking the
repository version. The backup path is printed during installation.
The work machine gets dotfiles only — no package management:
git clone https://github.com/nickromney/n-dotfiles.git
cd n-dotfiles
./stow.sh --dry-run # preview (shows conflicts with existing files)
./stow.sh # symlink everything
./stow.sh zsh git # or just selected packages./stow.sh --adopt pulls pre-existing real files into the repo if you
want to keep them — review with git diff afterwards.
./stow.sh --backup-conflicts instead preserves those files in a separate
backup and keeps the repository version authoritative. Plain ./stow.sh and
make stow remain conservative and fail before changing anything when a
conflict exists.
For a general Linux host where the prerequisites are already installed:
make linux-install # symlink dotfiles + install mise toolsBrewfile.posix remains available for Linux hosts that intentionally use
Homebrew, but it is not part of the Arch/Omarchy path.
For Omarchy/Arch, keep Omarchy's system packages and Super-key defaults, then
layer selected Stow packages and the separate Caps-Lock Hyper bindings on top.
There's no Brewfile on this path — ./bootstrap-omarchy.sh --dry-run
mechanizes the pacman + stow + Zsh login shell + keyd + mise sequence; see
omarchy/README.md for the full walkthrough and the parts
that stay manual (1Password sign-in, git SSH signing, mail client choice).
make install # host-appropriate package layer + stow + mise
make linux-install # Linux: stow + mise, no Homebrew
make omarchy-setup # Arch/Omarchy: pacman prerequisites + personal layers
make stow # symlink dotfiles only
make update # update brew, mise, mas (and rustup if present)
make configure # apply macOS settings (MACOS_PROFILE=personal|work)
make lint # shellcheck + markdownlint-cli2
make test # full BATS suite
make audit # package drift plus deep Homebrew/mise/PATH/app auditNote: Mac App Store installs require you to sign in via the App Store app and click "Get" once per app before
brew bundlecan install them viamas.
Add one line to mise/.config/mise/config.toml:
[tools]
kubectl = "1.36.4" # short name from `mise registry`
"github:cilium/hubble" = "1.19.4" # or any GitHub release directlyThen run mise install. The same entry works on macOS and Linux.
Useful commands:
mise registry <name> # check whether a tool has a short name
mise ls # what is installed/active
mise outdated # check pinned tools for newer releases
mise use <name>@<reviewed-version> # explicitly change a repository pin
mise use -g go@1.26.8 # example of an explicit global pinBefore changing pins, check each selected release's publication date and use
versions published at least seven days ago. Use mise install for the repository's existing locked
versions; avoid unconstrained latest-version installation or upgrade commands.
Add a cask/brew/mas line to the Brewfile and run
make install (or brew bundle --file Brewfile).
AudioPriorityBar owns its own build, installation, release, and notarisation
workflow in the sibling
~/Developer/personal/AudioPriorityBar checkout. Use
that repository's make dev, make install, or release workflow. This repo
only stows the stable preference configuration and provides
scripts/configure-audio-priority-bar.sh for applying it.
Borders likewise owns its app source, build, and release workflow in the
optional sibling ~/Developer/personal/borders checkout. The
macOS bootstrap and make install flows automatically run its make install
when that checkout is present, and skip it when it is absent. This repo only
stows the stable configuration and the borders command wrapper.
The audio-priority-bar Stow package links the stable priority configuration
to ~/.config/audio-priority-bar/preferences.plist. The installer merges
those keys into the app's com.example.AudioPriorityBar UserDefaults domain
on every run while preserving its timestamped knownDevices cache.
To find drift in either direction, run make audit — it reports package drift
and a deeper Homebrew/mise/PATH/application inventory. It reports
Brewfile entries missing from the machine and installed packages
missing from the Brewfile.
Private harness assets are reconciled from the optional sibling
../harnesses-private repo into the Claude skill root. The selected Josh
Pigford, Matt Pocock, JSM, and pstack skills from load/global.txt are also exposed through the shared
global and Codex views; other private providers remain Claude-only. Only
skills selected by provider load manifests are exposed. When two loaded
providers use the same skill directory name, each uses a deterministic
<provider>-<skill> destination name.
Run this from the n-dotfiles repo root:
./scripts/sync-private-harness-assets.sh --dry-run
./scripts/sync-private-harness-assets.sh --executeUse --private-root <path> if the private harness repo is not a sibling of
n-dotfiles.
Light-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
# Non-interactive apply with follow-up instructions instead of prompts
./_macos/macos.sh --no-input personal.yaml
# Equivalent Makefile helper
make 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 --profile personal --no-input
# Check what's available without downloading
./setup-ssh-from-1password.sh --profile personal --dry-run --no-inputIn 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 --profile personal --unsafe
# Non-interactive private-key download
./setup-ssh-from-1password.sh --profile personal --unsafe --yes --no-inputUse 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:
- The helper does not write fetched credentials to disk. AWS CLI role and login flows can cache credentials locally; keep
~/.awsmachine-local. See AWS credential caching. - 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 = git@github.com:OrgName/ insteadOf = https://github.com/OrgName/ [url "git@ado-work-2025-client-2:v3/ORG/PROJECT/"] insteadOf = git@ssh.dev.azure.com:v3/ORG/PROJECT/ [user] email = work@company.com
-
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)
- Secret sources: Credentials are fetched from 1Password. Unsafe SSH mode exports private keys and creates local backups; keep those files out of Git.
- Encryption boundary: 1Password protects its stored items. Downloaded keys, configs, and backups rely on host disk encryption and file permissions.
- Audit visibility: This repo does not implement a credential access log. 1Password usage reports depend on the account plan, permissions, and supported clients.
- Easy rotation: Update credentials in one place
- Team sharing: Safely share vaults with team members
- MFA protection: Enable 1Password two-factor authentication for account sign-in. It does not require a new second factor for every CLI credential read.
The dotfiles manage PATH configuration for Rust/Cargo in zsh/.zshrc. 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/.local/share/mise/shims- mise-managed tools (non-interactive shells)$HOME/Library/Application Support/com.silo.desktop/bin- Silo CLI (macOS)$HOME/.lmstudio/bin- LM Studio CLI
Each directory is only added if it exists, preventing errors on partial installations. Interactive zsh shells also run mise activate, which keeps tool versions in sync per directory.
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.
.
├── Brewfile # macOS layer: casks, fonts, mas apps, formulae
├── Brewfile.posix # Linux (Homebrew on Linux) formulae
├── bootstrap.sh # Fresh-Mac bootstrap: brew + Brewfile + stow + mise
├── stow.sh # Stow entrypoint (all a work machine needs)
├── Makefile # install / stow / update / configure / test targets
├── mise/ # Stow tree for ~/.config/mise/config.toml (CLI tools)
├── _macos/ # macOS system configuration
│ ├── macos.sh # macOS settings script
│ └── *.yaml # Settings profiles (personal, work)
├── _test/ # BATS suites, shellcheck, Lima smoke tests
└── */ # Stow directories for dotfiles
├── aerospace/ # Tiling window manager
├── git/ # Git configuration
├── nvim/ # Neovim config
├── tmux/ # Tmux config
├── zsh/ # Zsh configuration
└── ... # Other tool configsThe repository includes a test suite using BATS (Bash Automated Testing System):
# Install BATS (required for testing)
mise install # cross-platform: bats is in mise/.config/mise/config.toml
brew install bats-core # macOS/Homebrew-on-Linux, alternative
sudo pacman -S bats # Arch/Omarchy, alternative
sudo apt-get install bats # Ubuntu/Debian, alternative
# Run all tests
./_test/run_tests.sh
# Run a single suite
bats _test/makefile.batsLocal validation runs through lefthook.
# Install hooks
lefthook install
# or
make hooks
# Skip a hook for an intentional one-off
LEFTHOOK=0 git commit -m "message"
git push --no-verify
# Run the pre-push gate manually
lefthook run pre-push --forceA Lima-based Ubuntu 24.04 smoke test validates the Linux path:
Homebrew on Linux + Brewfile.posix, stow.sh in a fresh $HOME,
and the global mise config.
# 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-runSee _test/README.md for suite structure and the mocking framework.
- 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