Remote Mac setup and package management via SSH
Project description
macstrap
A CLI tool for setting up and managing Macs — no Ansible knowledge required.
Define what you want installed in plain text files, then run a single command to apply the full setup. Works both locally (on the Mac itself) and remotely (over SSH from another machine). Safe to re-run anytime; already-installed items are skipped.
Quick Start: Fresh Mac Mini
Got a brand-new Mac mini? Here's the fastest path from unboxing to fully configured:
Step 0: Get Python on a fresh Mac
A brand-new macOS has no Python, no git, no compilers. You need developer tools first.
Option A — Install Xcode from the App Store (recommended)
- Open the App Store on the Mac
- Search for Xcode and install it (~30 GB, takes a while)
- After installation, open Terminal and run:
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
python3 --version # should show Python 3.x
Option B — Install Command Line Tools only (smaller)
xcode-select --install
# A dialog will appear — click "Install" and wait
Note:
xcode-select --installrequires a GUI dialog. If your Mac mini is headless (no display), use the App Store method via Screen Sharing, or install viasoftwareupdate:sudo touch /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress LABEL=$(softwareupdate -l 2>&1 | grep -o "Label: Command Line Tools.*" | sort -r | head -1 | sed 's/Label: //') sudo softwareupdate -i "$LABEL" --verbose sudo rm -f /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress
Step 1: Install Homebrew + macstrap
macstrap requires Python 3.10+, but Xcode CLT only provides Python 3.9. Install Homebrew to get a newer Python:
# Install Homebrew (includes Python 3.12)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
eval "$(/opt/homebrew/bin/brew shellenv)"
# Install macstrap
pip3 install macstrap
Step 2: Local setup on the Mac
macstrap local enable-ssh
# ✓ Remote Login (SSH) enabled.
# This Mac is reachable at: mac-mini.local
# SSH user: user
Step 3: Remote setup from another machine
pip install macstrap
macstrap init # create package + settings files
macstrap ssh-auth mac-mini.local # register + store credentials
macstrap run mac-mini.local # apply full setup
Prerequisites summary
| What | Why |
|---|---|
| Xcode or Xcode CLT | Provides git, compilers, and Python 3.9 |
| Homebrew or MacPorts | Provides Python 3.10+ (required by macstrap) |
pip3 install macstrap |
Installs macstrap + Ansible |
Local Commands
Run these on the Mac itself for initial machine setup:
macstrap local enable-ssh
Enable the macOS SSH server (Remote Login) so other machines can connect:
% macstrap local enable-ssh
✓ Remote Login (SSH) enabled.
This Mac is reachable at: mac-mini.local
SSH user: user
Test from another machine:
ssh user@mac-mini.local
macstrap local install-xcode
Install Xcode Command Line Tools (git, compilers, Python, etc.):
% macstrap local install-xcode
✓ Xcode Command Line Tools already installed (v16.2.0.0.1.1733547573).
If not yet installed, macstrap uses softwareupdate to install non-interactively.
macstrap local install-brew
Install Homebrew package manager:
% macstrap local install-brew
✓ Homebrew 5.0.13 is already installed.
Automatically adds brew shellenv to ~/.zprofile and ~/.zshrc.
macstrap local install-macports
Install MacPorts package manager (auto-detects macOS version):
% macstrap local install-macports
✓ MacPorts is already installed (Version: 2.11.6).
Downloads the correct .pkg from GitHub releases for your macOS version.
Remote Commands
Run these from another machine to manage Macs over SSH.
1. Initialise config files
% macstrap init
% macstrap init --examples
This creates a settings.toml and package list files:
settings.toml # enable/disable roles (see below)
packages-macports.txt # MacPorts packages
packages-brew.txt # Homebrew formulae
packages-brew-casks.txt # Homebrew casks (GUI apps)
packages-npm-global.txt # Global npm packages (installed via nvm)
packages-pip-global.txt # Global pip packages
Edit the package files like a shopping list — one package per line, # for comments:
# packages-brew.txt
git
gh
fzf
ripgrep
Use --examples to also create starter config directories under examples/ (such as ai-cli, openclaw, utilities-dev, php8.3-dev).
2. Configure roles via settings.toml
Control which setup roles are enabled:
# settings.toml
[roles]
macports = false
homebrew = true
nvm = false
openjdk = false
docker = false
shell = true
pip = true
| Role | Default | What it does |
|---|---|---|
macports |
off | Installs MacPorts and packages from packages-macports.txt |
homebrew |
on | Installs Homebrew formulae from packages-brew.txt and casks from packages-brew-casks.txt |
nvm |
off | Installs nvm, Node.js (v22), and global npm packages from packages-npm-global.txt |
openjdk |
off | Configures Java symlink/PATH for Homebrew OpenJDK |
docker |
off | Checks Docker Desktop status (install via packages-brew-casks.txt) |
shell |
on | Deploys unified ~/.zshrc with PATH entries for enabled tools |
pip |
on | Installs global pip packages from packages-pip-global.txt |
Only enabled roles run during macstrap run. If settings.toml is missing, defaults apply.
3. Register a host
ssh-auth does two things:
- Copy your SSH public key to the remote Mac (one-time, requires SSH password)
- Store the sudo password in the system credential store
% macstrap ssh-auth mac-mini.local
% macstrap ssh-auth mac-mini.local --user remoteuser
% macstrap ssh-auth 192.168.1.100 --user remoteuser
If SSH key auth is already set up, skip the key copy:
% macstrap ssh-auth mac-mini.local --skip-key-copy
You can register multiple machines — each gets its own credential entry.
4. Run setup
% macstrap run
% macstrap run mac-mini.local
% macstrap run 192.168.1.100
% macstrap run remoteuser@mac-mini.local
% macstrap run --config examples/ai-cli --config examples/openclaw 192.168.1.100
macstrap connects over SSH, reads your settings.toml and package files, and installs everything that isn't already there. First run takes 30-60 minutes (MacPorts compiles from source). Subsequent runs take about 1 minute.
SSH user resolution order (highest priority first):
--userflaguser@hostinline syntax- SSH user stored at
ssh-authtime - Local OS username (fallback)
5. Verify installation
Check whether every package in your files is installed on the remote Mac:
% macstrap verify
% macstrap verify mac-mini.local
% macstrap verify 192.168.1.100 --user remoteuser
Output example:
Tools
✓ brew Homebrew 4.4.12
✓ port Version: 2.12.3
✓ nvm 0.40.1
✓ node v22.14.0
✗ docker not found
✓ java openjdk version "21.0.7" 2025-04-15
✓ python3 Python 3.13.2
Homebrew (2/3)
✓ git
✓ ripgrep
✗ htop (missing)
⚠ 2 installed, 1 missing
Exits with code 0 if everything is present, 1 if anything is missing — useful in CI or scripts.
Run only part of the setup
Use --tag to apply a single role:
% macstrap run --tag homebrew
% macstrap run mac-mini.local --tag macports
% macstrap run --tag nvm
% macstrap run --tag shell
% macstrap run --tag pip
Available tags: macports homebrew nvm npm docker openjdk shell pip
Note:
--tagselects which roles to consider, butsettings.tomlis the master switch. A role disabled in settings will be skipped even if selected by--tag.
Dry run
Preview what would change without applying:
% macstrap run --check
% macstrap run mac-mini.local --check
Manage registered hosts
% macstrap list
macstrap credential store (macOS Keychain)
Default target: mac-mini.local
Host User Password
mac-mini.local remoteuser ✓ stored ← default
192.168.1.200 admin ✓ stored
% macstrap delete mac-mini.local
% macstrap delete-all
Example: install OpenJDK + Docker Desktop
macstrap init
# Enable the roles in settings.toml
# [roles]
# homebrew = true
# openjdk = true
# docker = true
echo "openjdk" >> packages-brew.txt
echo "docker" >> packages-brew-casks.txt
macstrap ssh-auth mac-mini.local
macstrap run mac-mini.local
macstrap verify mac-mini.local
After Docker Desktop is installed, sign in to the target Mac desktop and launch Docker once to accept the licence and complete first-run setup.
What gets installed
macstrap first bootstraps the target Mac over raw SSH (installing Xcode CLT if missing), then runs enabled Ansible roles:
| Phase | Role | What it does |
|---|---|---|
| Bootstrap | (raw SSH) | Installs Xcode Command Line Tools via softwareupdate if missing |
| Main | macports |
Installs MacPorts and packages from packages-macports.txt |
| Main | homebrew |
Installs Homebrew formulae and casks |
| Main | nvm |
Installs nvm, Node.js, and global npm packages |
| Main | docker |
Checks Docker Desktop status |
| Main | openjdk |
Configures Java symlink/PATH for Homebrew OpenJDK |
| Main | shell |
Deploys unified ~/.zshrc with PATH entries for all tools |
| Main | pip_global |
Installs global pip packages |
All roles are idempotent — re-running only installs what is missing. Each role is controlled by settings.toml and only runs when enabled.
Fresh Mac note
On a brand-new Mac, macOS intercepts /usr/bin/python3 and shows an installation dialog instead of running Python. macstrap handles this automatically over SSH: the bootstrap phase uses raw SSH commands (no Python needed) to install Xcode CLT first, then hands off to the normal Ansible playbook.
Credential storage
| Platform | Storage location |
|---|---|
| macOS | Keychain (security command) — visible in Keychain Access |
| Linux | ~/.config/macstrap/ directory (chmod 600) |
Key naming convention:
macstrap-target → current default host
macstrap-hosts → comma-separated index of all registered hosts
macstrap-pass-{host} → sudo password for a specific host
macstrap-user-{host} → SSH username for a specific host
Requirements
- Python 3.10+
- SSH access to the target Mac (key-based auth is set up automatically by
ssh-auth) - macOS 13+ on the target machine
Ansible is bundled as a dependency — pip install macstrap is all you need.
License
MIT © changyy
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file macstrap-1.2.0.tar.gz.
File metadata
- Download URL: macstrap-1.2.0.tar.gz
- Upload date:
- Size: 28.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
08b5ae573432ebd49f3810d7b83515abff25a3badc9e48df5531aa59a86ac8c5
|
|
| MD5 |
0736329dd3228aeeb305d84aadd7d4b7
|
|
| BLAKE2b-256 |
a2296e19f5c9a578434f4bd6638bd301a000b771121facdcd494bd33667bfca4
|
Provenance
The following attestation bundles were made for macstrap-1.2.0.tar.gz:
Publisher:
python-publish.yml on changyy/py-macstrap
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
macstrap-1.2.0.tar.gz -
Subject digest:
08b5ae573432ebd49f3810d7b83515abff25a3badc9e48df5531aa59a86ac8c5 - Sigstore transparency entry: 1217606419
- Sigstore integration time:
-
Permalink:
changyy/py-macstrap@bd4cb97817a223fb8e85e6edc2377c9ad677401c -
Branch / Tag:
refs/tags/1.2.0 - Owner: https://github.com/changyy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@bd4cb97817a223fb8e85e6edc2377c9ad677401c -
Trigger Event:
release
-
Statement type:
File details
Details for the file macstrap-1.2.0-py3-none-any.whl.
File metadata
- Download URL: macstrap-1.2.0-py3-none-any.whl
- Upload date:
- Size: 46.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
088501db198da512b57bf68dbb49d8b75efc7e5f1df73d67bc8d5277bd53a3ee
|
|
| MD5 |
38a228c958c2608a271a78da8027d6c6
|
|
| BLAKE2b-256 |
2d808e1046739581b4772a1ed6e0a07c81354fc4cd0d68b26a8ccc0bf5ac64ae
|
Provenance
The following attestation bundles were made for macstrap-1.2.0-py3-none-any.whl:
Publisher:
python-publish.yml on changyy/py-macstrap
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
macstrap-1.2.0-py3-none-any.whl -
Subject digest:
088501db198da512b57bf68dbb49d8b75efc7e5f1df73d67bc8d5277bd53a3ee - Sigstore transparency entry: 1217606452
- Sigstore integration time:
-
Permalink:
changyy/py-macstrap@bd4cb97817a223fb8e85e6edc2377c9ad677401c -
Branch / Tag:
refs/tags/1.2.0 - Owner: https://github.com/changyy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@bd4cb97817a223fb8e85e6edc2377c9ad677401c -
Trigger Event:
release
-
Statement type: