Skip to content

About

Cross-platform system bootstrap for macOS and Ubuntu

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Latest commit

 

History

165 Commits

Folders and files

Repository files navigation

ns-bootstrap

Cross-platform system bootstrap for macOS and Ubuntu 24.04, both using zsh.

Table of Contents

Quick Start

git clone https://github.com/ns408/ns-bootstrap.git ~/ns-bootstrap
cd ~/ns-bootstrap
./install/bootstrap.sh                  # Full install (admin)
./install/bootstrap.sh --dotfiles-only  # Dotfiles only (non-admin)
./install/bootstrap.sh --relink         # Re-point symlinks after moving the repo
./install/bootstrap.sh --dry-run        # Preview what would be installed

Profiles

Profile Includes
minimal Core CLI tools, modern replacements (ripgrep, fd, fzf, bat, zoxide), browsers, utilities
developer Minimal + languages (Python, Go, Ruby), Docker, OrbStack, AWS CLI, dev tools
cloud-engineer Developer + Terraform, Azure CLI, Kubernetes, security tools, Wireshark

Architecture

flowchart TD
    A([bootstrap.sh]) --> B[detect OS]
    B --> C[select profile<br/>minimal · developer · cloud-engineer]
    C --> D[install packages<br/>brew bundle / apt]
    D --> E[install modern tools<br/>ripgrep fd fzf bat zoxide ...]
    E --> F[symlink dotfiles<br/>.zshrc .vimrc .gitconfig ...]
    F --> G[install git hooks<br/>secret scan · AI trailer strip]
    G --> H[init secrets<br/>1Password / pass]
    H --> I[schedule updates<br/>launchd on macOS]

    F -.->|on shell open| J[loader.sh]
    J --> K[functions/ aliases/ platform/]
Loading

See .ai/ARCHITECTURE.md for detailed diagrams covering profiles, secrets, git hooks, two-account setup, and CI.

Structure

dotfiles/          # Config templates (.gitconfig, .zshrc, starship.toml, etc.)
shell/             # Functions, aliases, platform-specific scripts
packages/          # Brewfiles (macOS) and apt-packages (Ubuntu)
install/           # Bootstrap installer and tool scripts
secrets/           # Secrets management bootstrap
scripts/           # Scheduled updates, launchd agents
.ai/               # Project documentation and knowledge base
  CONTEXT.md       #   Project conventions and structure
  ARCHITECTURE.md  #   System architecture diagrams
  knowledge/       #   Domain knowledge (macOS, shell, security, etc.)

Documentation

The .ai/ directory contains structured project documentation and domain knowledge accumulated while building this repo:

File Topics
macos-admin.md Two-account setup, launchd, Homebrew patterns, App Store CLI
shell-cross-platform.md Bash/zsh portability, compinit, template variables
secrets-management.md 1Password structured items, pass, abstraction patterns
scheduled-updates.md launchd/systemd timers, tmux sessions, log management
security-hardening.md Git hooks, gitleaks, npm security, SSH signing
modern-cli-tools.md Tool migration guide, fzf/atuin/mise integration

Secrets

Secrets are managed via 1Password CLI (macOS) or pass (Ubuntu). No secrets are stored in this repo.

source shell/functions/secrets.sh
get_secret "git/email-personal"
set_secret "github/token" "ghp_..."

Modern CLI Tools

Traditional Modern Description
grep ripgrep (rg) Faster recursive search
find fd Simpler syntax, respects .gitignore
cat bat Syntax highlighting, line numbers
ls eza Colors, git status, tree view
cd zoxide (z) Jump by frecency
top btop Visual process/resource monitor
du dust Intuitive disk usage
diff delta Side-by-side git diffs
dig doggo DNS with DoH/DoT, colored output
curl httpie Human-friendly HTTP client
Ctrl+R atuin Full-text history search, cross-machine sync

Run modern-tools-help for a full reference.

Shell

Both macOS and Ubuntu use zsh with the same stack:

  • Framework: Oh-My-Zsh (plugins, completions, git aliases). Oh-My-Zsh, its add-on plugins and the vim plugins are checked out at commits pinned in packages/git-pins. A monthly workflow proposes moving each pin to the commit its upstream branch held 14 days earlier, by GitHub's push records, and holds back any repository whose branch was force-pushed. After you merge a bump and git pull the clone, each account applies the new pins the next time it opens a shell.
  • Prompt: Starship (Rust-based, cross-shell, context-aware)
  • History: Atuin (SQLite-backed, fuzzy search, encrypted sync)
  • Enhancements: zsh-autosuggestions, zsh-syntax-highlighting, fzf-tab

Python: pip refuses to install outside a virtualenv (dotfiles/pip/pip.conf, linked into ~/.config/pip/); override a single command with PIP_REQUIRE_VIRTUALENV=0. An existing pip.conf of your own is kept, not replaced. uv is set never to download a Python of its own (UV_PYTHON_DOWNLOADS=never); override with UV_PYTHON_DOWNLOADS=automatic.

Ubuntu specifics: zsh is installed via apt and set as the default shell via usermod. Dotfiles used: dotfiles/shell/.zshrc.ubuntu and dotfiles/shell/.zprofile.ubuntu (same as macOS but without macOS-only plugins and with Linux PATH setup).

macOS Initial Setup

Two-Account Setup

This repo is designed for a two-account macOS workflow:

  • Admin account — installs Homebrew packages, system updates, and privileged operations
  • Daily account — non-admin, used for day-to-day development work

Shared files (repositories, data, configs) live in one directory both accounts can reach, readable by the staff group they both belong to.

Shared directory permissions — Run once from the admin account to grant the staff group (which both accounts belong to) full read/write access with inheritance:

sudo chmod -R +a \
  "group:staff allow list,add_file,search,add_subdirectory,delete_child,readattr,writeattr,readextattr,writeextattr,readsecurity,file_inherit,directory_inherit" \
  /Users/<daily>/repos

Bootstrap workflow:

# 1. As admin account — full install (packages + dotfiles + secrets)
./install/bootstrap.sh

# 2. As daily account — dotfiles only (symlinks + oh-my-zsh + secrets)
./install/bootstrap.sh --dotfiles-only

Moving the repo: every account carries its own symlinks into the repo, so moving the clone orphans each of them. Run the relink from each account — an account cannot see, let alone repair, links inside another account's 700 directories (~/.config/atuin/ is one), and a dangling link makes tools that create their config on first run fail with a confusing No such file or directory.

# 1. Move the clone, then from the new location, once per account:
./install/bootstrap.sh --relink   # re-points symlinks + NS_BOOTSTRAP_DIR, no prompts

# 2. If the LaunchDaemon is in use, re-render it with the new path (admin account):
sudo launchctl bootout system/com.ns-bootstrap.update-daily
#    re-install from scripts/launchd-daemon/*.daemon.plist.template (see its header)

# 3. Open a new login shell in each account to confirm it starts clean.

Scheduled Updates

On macOS, bootstrap installs one scheduled job (launchd). On Ubuntu there is none of its own: daily security updates come from Ubuntu's unattended-upgrades, which bootstrap installs and apt's own timer runs as root.

Schedule What Requires
07:00 daily update-brew-daily: Homebrew formulae only No interaction

update-my-system (casks, mise, App Store, softwareupdate) is on demand: nothing schedules it, so run it yourself when you are at the machine. The App Store and macOS updates need an active GUI session, some casks prompt for sudo, and macOS installs can reboot. Pinned oh-my-zsh and plugins don't depend on it: each account applies new pins the next time it opens a shell.

Two accounts: the default daily job is a LaunchAgent in ~/Library/LaunchAgents/, and a LaunchAgent only runs while its owner is logged in to the GUI. If the account that owns Homebrew is not your daily login, as in the two-account setup above, install the LaunchDaemon from scripts/launchd-daemon/ instead. It runs as the Homebrew owner whoever is logged in, and bootstrap warns when it detects this mismatch.

Note: mas (Mac App Store) and softwareupdate require an active Aqua/GUI session and will not work via su or SSH.

To run updates manually:

update-brew-daily    # Quick — formulae only
update-my-system     # Full — casks, mise, pinned omz and plugins, softwareupdate list
update-macos-install # Manual — install macOS updates (may reboot)

Hostname Configuration

YOUR_HOSTNAME="my-macbook-pro-m2"
sudo scutil --set HostName "$YOUR_HOSTNAME"
sudo scutil --set ComputerName "$YOUR_HOSTNAME"
sudo scutil --set LocalHostName "$YOUR_HOSTNAME"

Essential System & Developer Setup

  • Update macOS: sudo softwareupdate -i -a --restart
  • Xcode CLI Tools: xcode-select --install
  • Trackpad/Mouse: Adjust tracking speed, tap to click, natural scrolling
  • Keyboard: Set key repeat rate, enable Full Keyboard Access
  • Finder: Show all extensions and hidden files (Cmd+Shift+.)
  • Clear Dock: defaults write "com.apple.dock" "persistent-apps" -array && killall Dock
  • Enable Firewall: sudo defaults write /Library/Preferences/com.apple.alf globalstate -int 1
  • Enable FileVault: sudo fdesetup enable
  • Disable Homebrew Analytics: brew analytics off

Remote Desktop (Ubuntu, optional)

Not part of the default bootstrap. install/ubuntu/install-remote-desktop.sh sets up an xrdp server, then restricts port 3389 to your local subnet via ufw. If it has to enable ufw, it allows SSH first so it can't lock you out; an already-active ufw keeps its existing SSH rules:

bash install/ubuntu/install-remote-desktop.sh          # interactive (XFCE)
bash install/ubuntu/install-remote-desktop.sh --yes    # no prompt
DESKTOP=gnome bash install/ubuntu/install-remote-desktop.sh      # opt into GNOME
SUBNET=10.0.0.0/24 bash install/ubuntu/install-remote-desktop.sh  # override subnet
  • Desktop: defaults to lightweight XFCE (most reliable over RDP). GNOME is opt-in via DESKTOP=gnome.
  • GPU acceleration: the script disables xorgxrdp glamor (forces software rendering) so the X backend works on any GPU. GNOME Shell crashes on software GL, so DESKTOP=gnome only works on hosts with functioning GPU acceleration; everywhere else stick with the XFCE default.
  • Session fallback: in the XFCE path the script pins the system default session manager to XFCE. If ~/.xsession ever goes missing, xrdp would otherwise fall back to GNOME and black-screen/crash on a GPU-less host; pinning prevents that silent fallback.
  • Subnet: auto-detected; override with SUBNET=<cidr> for multi-NIC hosts. The script refuses an auto-detected subnet that isn't a private range (RFC1918 or CGNAT), so a public IP on the NIC can't expose RDP by accident.
  • Polkit: the script installs an override that silences the Ubuntu 24.04 "authentication required to create a color profile" popups over RDP.
  • One session per user: do not stay logged into the physical console as the same user you RDP in as. GNOME/XFCE refuse two simultaneous sessions and you get a black screen or crash (loginctl list-sessions to check).
  • Untrusted network: prefer an SSH tunnel over the LAN rule: ssh -L 3389:localhost:3389 user@host, then point your RDP client at localhost:3389.

Platform Compatibility

Platform Architecture Status
macOS (Apple Silicon) ARM64 Tested
Ubuntu 24.04 LTS ARM64 Tested (SSH)
macOS (Intel) x86_64 Expected to work, untested
Ubuntu 24.04 LTS x86_64 Expected to work, untested

Breaking Changes

This is a personal setup, public for reference, with no compatibility promise: if you depend on it, fork it or pin a commit. Changes that alter an already-bootstrapped machine migrate it automatically when bootstrap runs again, never overwrite configuration you own, and are marked Breaking in their pull request.

License

MIT

About

Cross-platform system bootstrap for macOS and Ubuntu

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages