Remote Mac setup and package management via SSH
Project description
macstrap
A CLI tool for setting up and managing remote Macs via SSH — no Ansible knowledge required.
Define what you want installed in plain text files, register your target machines once, then run a single command to apply the full setup. Safe to re-run anytime; already-installed items are skipped.
Installation
% pip install macstrap
Usage
1. Initialise package files
% macstrap init
% macstrap init --examples
This creates template files you edit to declare what gets installed:
packages-macports.txt # MacPorts packages
packages-brew.txt # Homebrew formula
packages-brew-casks.txt # Homebrew cask (GUI apps)
packages-npm-global.txt # Global npm packages (installed via nvm default Node)
packages-pip-global.txt # Global pip packages
Edit them like a shopping list — one package per line, # for comments:
# packages-brew.txt
git
gh
fzf
ripgrep
For example:
- Add
openjdktopackages-brew.txtto install OpenJDK. - Add
dockertopackages-brew-casks.txtto install Docker Desktop.
If you use --examples, macstrap also creates starter config directories under examples/ (such as examples/ai-cli, examples/openclaw, examples/utilities-dev, examples/php8.3-dev).
2. Register a host
ssh-auth does two things in sequence:
- 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 # if remote username differs
% macstrap ssh-auth 192.168.1.100 --user remoteuser
If SSH key auth is already set up, skip the key copy step:
% macstrap ssh-auth mac-mini.local --skip-key-copy
The SSH username is saved alongside the password so you don't need to type it on every run.
You can register multiple machines — each gets its own credential entry.
3. Run setup
% macstrap run
% macstrap run mac-mini.local
% macstrap run 192.168.1.100
% macstrap run remoteuser@mac-mini.local # override SSH user inline
% macstrap run --config examples/ai-cli --config examples/openclaw 192.168.1.100
macstrap connects over SSH, reads your 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)
4. Verify installation
Check whether every package in your package files is actually 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.
Example: install OpenJDK + Docker Desktop
% macstrap init
% echo "openjdk" >> packages-brew.txt
% echo "docker" >> packages-brew-casks.txt
% macstrap ssh-auth mac-mini.local
% macstrap run mac-mini.local --tag homebrew --tag openjdk --tag docker
% 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.
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
Run only part of the setup
Use --tag to apply a single role instead of the full playbook:
% 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
Dry run
Preview what would change without applying anything:
% macstrap run --check
% macstrap run mac-mini.local --check
What gets installed
macstrap reads your package files and applies the following roles in order:
| Phase | Role | What it does |
|---|---|---|
| Bootstrap | (raw SSH) | Installs Xcode Command Line Tools via softwareupdate if missing — required before Python is available on a fresh Mac |
| Main | macports |
Installs MacPorts and packages from packages-macports.txt |
| Main | homebrew |
Installs Homebrew formulae from packages-brew.txt and casks from packages-brew-casks.txt |
| Main | nvm |
Installs nvm, Node.js (v22), and global npm packages from packages-npm-global.txt |
| Main | docker |
Checks Docker Desktop status and first-launch readiness (install is driven by packages-brew-casks.txt, e.g. docker) |
| Main | openjdk |
Configures Java symlink/PATH for Homebrew OpenJDK (install is driven by packages-brew.txt, e.g. openjdk) |
| Main | shell |
Deploys a unified ~/.zshrc with PATH entries for MacPorts, Homebrew, NVM, Java |
| Main | pip_global |
Installs global pip packages from packages-pip-global.txt |
All roles are idempotent — re-running only installs what is missing.
Fresh Mac note
On a brand-new Mac (or one that has never run Xcode tools), macOS intercepts /usr/bin/python3 and shows an installation dialog instead of running Python. macstrap handles this automatically: 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 → Passwords → search macstrap |
| Linux | ~/.config/macstrap/ directory (each key is a separate file, 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.1.0.tar.gz.
File metadata
- Download URL: macstrap-1.1.0.tar.gz
- Upload date:
- Size: 22.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a3605378b924e6d87771e4ff6ce297998cc86e1425a1a98e882a279b894a3c27
|
|
| MD5 |
2e52bee2b31f7b3ec249ffe3195d2ba9
|
|
| BLAKE2b-256 |
2beeb16c6ffb7e67dbc46a6b77e11215f5b01dd9658586621d14804de64e7595
|
Provenance
The following attestation bundles were made for macstrap-1.1.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.1.0.tar.gz -
Subject digest:
a3605378b924e6d87771e4ff6ce297998cc86e1425a1a98e882a279b894a3c27 - Sigstore transparency entry: 1075647709
- Sigstore integration time:
-
Permalink:
changyy/py-macstrap@595b4b8b0b0cc3951423c991dd58ab6485730a6c -
Branch / Tag:
refs/tags/1.1.0 - Owner: https://github.com/changyy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@595b4b8b0b0cc3951423c991dd58ab6485730a6c -
Trigger Event:
release
-
Statement type:
File details
Details for the file macstrap-1.1.0-py3-none-any.whl.
File metadata
- Download URL: macstrap-1.1.0-py3-none-any.whl
- Upload date:
- Size: 38.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 |
a47ee2e355eea6700aa0b0913dd94baaeda4ca1cda0eaf853f70b91f602a93cc
|
|
| MD5 |
4994fdd8012ff6b3f79bc88aa6d68568
|
|
| BLAKE2b-256 |
82a71770ee525d464aeafd38ada8e8e7e76a22c98f230c232718992c76c47f69
|
Provenance
The following attestation bundles were made for macstrap-1.1.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.1.0-py3-none-any.whl -
Subject digest:
a47ee2e355eea6700aa0b0913dd94baaeda4ca1cda0eaf853f70b91f602a93cc - Sigstore transparency entry: 1075647759
- Sigstore integration time:
-
Permalink:
changyy/py-macstrap@595b4b8b0b0cc3951423c991dd58ab6485730a6c -
Branch / Tag:
refs/tags/1.1.0 - Owner: https://github.com/changyy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@595b4b8b0b0cc3951423c991dd58ab6485730a6c -
Trigger Event:
release
-
Statement type: