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
sudo xcodebuild -license accept # accept the Xcode license (required before python3 works)
python3 --version # should show Python 3.9.x
Note: Until you accept the license,
python3will print a license error instead of running. Thepythoncommand does not exist on macOS — always usepython3.
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 macstrap
curl -fsSL https://raw.githubusercontent.com/changyy/py-macstrap/main/install.sh | bash
This single command:
- Creates a venv at
~/.macstrap/venv(built-in pip 21.x is too old, so venv + upgrade is needed) - Installs macstrap with Python 3.9
- Adds
macstrapto your PATH
Alternative (for development):
git clone https://github.com/changyy/py-macstrap.git
cd py-macstrap && python3 -m venv .venv && source .venv/bin/activate && pip install --upgrade pip && pip install -e .
Step 2: Local setup on the Mac
source ~/.zshenv # or open a new terminal
macstrap local upgrade-python # install Homebrew + Python 3.10+
macstrap local enable-ssh # enable SSH server
macstrap local setup-server-mode # configure always-on server settings
Step 3: Unlock remote features
Remote commands (macstrap run, macstrap verify, etc.) require Python 3.10+ and ansible-core. After Step 1:
/opt/homebrew/bin/python3 -m pip install macstrap[remote]
Step 4: Remote setup from another machine
pip install macstrap[remote]
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 |
pip3 install macstrap |
Local commands work immediately on Python 3.9 |
| Homebrew (optional) | Provides Python 3.10+ for remote commands |
pip3 install macstrap[remote] |
Adds ansible-core for remote Mac setup |
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.
macstrap local upgrade-python
Ensure Python meets the minimum version for remote commands (currently 3.10+):
% macstrap local upgrade-python
Current Python: 3.9.6
Required: 3.10+ (for remote commands)
Installing python@3.12 via Homebrew...
✓ Python 3.12.x installed via Homebrew.
Next step: reinstall macstrap with remote support using the new Python:
/opt/homebrew/bin/python3 -m pip install macstrap[remote]
If Python already meets the requirement:
% macstrap local upgrade-python
✓ Python 3.12.8 already meets the minimum (3.10+).
macstrap local setup-server-mode
Configure macOS for always-on server use (ideal for a headless Mac mini):
% macstrap local setup-server-mode
Configuring server mode...
You may be prompted for your sudo password.
✓ 永不休眠 (system sleep disabled)
✓ 螢幕永不關閉 (display sleep disabled)
✓ 硬碟永不休眠 (disk sleep disabled)
✓ 斷電後自動重新開機 (auto restart on power loss)
✓ 網路喚醒 (Wake-on-LAN enabled)
✓ 5 setting(s) applied, 0 already correct.
Safe to re-run — already-correct settings are skipped.
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
- Local commands: Python 3.9+ (included with Xcode / Xcode CLT)
- Remote commands: Python 3.10+ and
pip install macstrap[remote](adds ansible-core) - SSH access to the target Mac (key-based auth is set up automatically by
ssh-auth) - macOS 13+ on the target machine
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.5.0.tar.gz.
File metadata
- Download URL: macstrap-1.5.0.tar.gz
- Upload date:
- Size: 34.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
225e36b3fc7457fe88cf32e4c531670d3352149ddef2e038153a113f2f1d5943
|
|
| MD5 |
95985e53a45ad51ecbb4c25fb9338ef7
|
|
| BLAKE2b-256 |
7d5d7cef7163fed900603938ad0351b9e235cfa998df79a835da1bb405f34a7b
|
Provenance
The following attestation bundles were made for macstrap-1.5.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.5.0.tar.gz -
Subject digest:
225e36b3fc7457fe88cf32e4c531670d3352149ddef2e038153a113f2f1d5943 - Sigstore transparency entry: 1247499028
- Sigstore integration time:
-
Permalink:
changyy/py-macstrap@0579c3d11cf3a8c16b49f5536c4c422485710e5b -
Branch / Tag:
refs/tags/1.5.0 - Owner: https://github.com/changyy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@0579c3d11cf3a8c16b49f5536c4c422485710e5b -
Trigger Event:
release
-
Statement type:
File details
Details for the file macstrap-1.5.0-py3-none-any.whl.
File metadata
- Download URL: macstrap-1.5.0-py3-none-any.whl
- Upload date:
- Size: 56.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ba2944a4af0de71a48864bca7f3f29b3a052df339614922695bb8798d7b5e5fe
|
|
| MD5 |
0f7201174e2a9d798e5efe4884378ff3
|
|
| BLAKE2b-256 |
2c323d2d1100315fca45b134cf33c3c10e0d0a0c9f895947940ed5c1e753d614
|
Provenance
The following attestation bundles were made for macstrap-1.5.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.5.0-py3-none-any.whl -
Subject digest:
ba2944a4af0de71a48864bca7f3f29b3a052df339614922695bb8798d7b5e5fe - Sigstore transparency entry: 1247499032
- Sigstore integration time:
-
Permalink:
changyy/py-macstrap@0579c3d11cf3a8c16b49f5536c4c422485710e5b -
Branch / Tag:
refs/tags/1.5.0 - Owner: https://github.com/changyy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@0579c3d11cf3a8c16b49f5536c4c422485710e5b -
Trigger Event:
release
-
Statement type: