LazyMount-Mac

📖 中文文档
Expand your Mac storage effortlessly — Auto-mount SMB shares and cloud storage at boot, with zero manual intervention.
Project Structure
.
├──README.md — English Documentation
├──README_CN.md — Chinese Documentation
├──CHANGELOG.md — Version history and migration notes
├──LICENSE — MIT License
├──install.sh / install.py — Per-user install, update, migration and rollback
├──mount_manager.sh — SMB/Rclone configuration and worker launcher
├──lazymount_runtime.py — DiskImages2/APFS verification and recovery
├──mount_manager.example.local.sh — Configuration example (installer preserves actual local config)
├──com.lazymount.plist — Template; generated on install
├──tests — Unit and opt-in APFS integration tests
└──docs/validation-v3.md — Verification report and remaining limits
Table of Contents
LazyMount-Mac runtime execution — network reachability check, credentials verification, and automated mount reconciliation
Why LazyMount?
Mac storage is expensive — a 1TB upgrade can cost $200+. LazyMount solves this by seamlessly extending your Mac with external storage:
- Game Libraries — Store Steam/Epic games on a NAS, play them like local installs
- Time Machine Backups — Back up to a remote server automatically
- Media Libraries — Access your movie/music collection stored on a home server
- Project Archives — Keep large files on cheaper storage, access them on-demand
- Cloud Storage — Mount Google Drive, Dropbox, or any rclone-supported service as a local folder
- AI Model Storage — Run large LLMs (Ollama) from network storage to save 100GB+ SSD space
Key features (v3.0.2):
- Per-user installer — resolves the home directory automatically, migrates legacy launchd Labels, and preserves local settings
- Independent SMB and Rclone workers — a degraded APFS image does not stop network and cloud shares
- APFS write verification — exclusive 4 KiB create/write/fsync/readback/cleanup;
df alone is not a health check
- DiskImages2 where supported — newer macOS uses
diskutil image attach; legacy systems retain hdiutil without mixing backends on failure
- Conservative recovery — UUID-verified normal eject/reattach, idle checks, cooldown, and backup-approved offline First Aid only if necessary
- Tailscale-compatible — network reachability and retry without hardcoded local usernames
Important: APFS sparsebundles on a network share are not equivalent to local SSD storage; stable SMB access, free physical NAS space, and backups still matter. Large images can take minutes for diskutil image attach to finish gathering band statistics even after the volume appears.
Architecture & Lifecycle
graph TD
Login["macOS user login"] --> Agent["launchd: com.lazymount"]
Agent --> Runtime["Python v3 runtime and process lock"]
Runtime --> SMB["SMB worker"]
Runtime --> Rclone["Rclone worker"]
Runtime --> APFS["APFS worker: UUID and physical-store identity"]
APFS --> Backend{"DiskImages2 available?"}
Backend -- Yes --> Modern["diskutil image attach"]
Backend -- No --> Legacy["hdiutil legacy attach"]
Modern --> Probe["Exclusive write / fsync / readback"]
Legacy --> Probe
Probe -- Pass --> Healthy["Healthy / Verified"]
Probe -- Fail --> Degraded["Degraded: classify errno"]
Degraded --> Safe["Idle check / normal reattach / cooldown"]
Safe --> Backup{"Offline repair approved?"}
Backup -- Yes --> Aid["verifyVolume / repairVolume / verifyVolume if needed"]
Backup -- No --> Degraded
Installation
Prerequisites
-
Rclone (for cloud storage mounting):
brew install rclone
# Then configure your remote:
rclone config
-
Install FUSE Interface (Connection Layer):
You need ONE of the following. FUSE-T is recommended for modern Macs (M1/M2/M3) as it doesn’t require lowering system security.
| Feature |
Option A: FUSE-T (Recommended) |
Option B: macFUSE (Legacy) |
| Type |
User-space (NFS Bridging) |
Kernel Extension (Kext) |
| Security |
✅ Safe (No SIP changes) |
Low (Must reduce security in Recovery Mode) |
| Stability |
✅ High (Uses native macOS NFS) |
Risk of kernel panics |
| Best for |
macOS 12+ / Apple Silicon (M-Series) |
Intel Macs / Legacy software |
To install FUSE-T (Recommended):
brew tap macos-fuse-t/homebrew-cask
brew install fuse-t
To install macFUSE (If you prefer Kexts):
brew install --cask macfuse
⚠️ How to enable macFUSE on Apple Silicon (M1/M2/M3/M4...)
Method 1: The Official Way (Recommended, keeps System Integrity Protection ON)
- Shutdown your Mac.
- Press and hold the Power Button until “Loading startup options” appears.
- Click Options -> Continue.
- Menu bar: Utilities -> Startup Security Utility.
- Select your disk -> Security Policy…
- Choose Reduced Security and check “Allow user management of kernel extensions…”.
- Restart -> System Settings -> Privacy & Security -> Allow “Benjamin Fleischer”.
- Restart again.
Method 2: The Terminal Way (NOT Recommended, disables SIP)
You might see tutorials suggesting csrutil disable in Recovery Terminal. While this works, it completely disables macOS security protections. Method 1 is safer.
-
(Recommended) Tailscale — For remote access to home network:
brew install --cask tailscale
Quick Start
Requires Python 3.9+ (python.org or Homebrew) and macOS with APFS support. The installer handles Intel / Apple Silicon paths. Run as your login user without sudo. New installations start with mount modules disabled; enable them in your local config. Existing settings and Keychain are preserved.
git clone https://github.com/yuanweize/LazyMount-Mac.git
cd LazyMount-Mac
./install.sh install
nano ~/Scripts/mount_manager.local.sh
./install.sh restart
./install.sh status
The installer resolves HOME/UID, generates com.lazymount, and migrates old services by their actual plist Label. Do not copy the repository plist template directly. SMB_USER is the network account, independent of your Mac login name.
⚙️ Configuration
Configuration is managed through a local config file (mount_manager.local.sh) that overrides the default settings in mount_manager.sh. This approach keeps your settings separate from the main script, making updates easier.
Using Local Config File (Recommended)
The script automatically loads mount_manager.local.sh from the same directory if it exists. This allows you to:
- Keep your settings separate from the main script
- Update
mount_manager.sh without losing your configuration
- Track changes to the main script via git while ignoring your local config
Setup:
# The installer creates this if absent; never overwrite existing local settings
nano ~/Scripts/mount_manager.local.sh
Example mount_manager.local.sh:
# --- Global Settings ---
# v3 releases update via ./install.sh update; AUTO_UPDATE_ENABLED is ignored
# --- Rclone Configuration ---
RCLONE_REMOTE="myremote:/path/to/folder"
RCLONE_MOUNT_POINT="$HOME/Mounts/CloudStorage"
RCLONE_IP="100.x.x.x" # Tailscale IP or 8.8.8.8
# --- SMB Configuration ---
SMB_IP="192.168.1.100" # Tailscale IP for remote access
SMB_USER="your_username"
SMB_SHARE="SharedFolder"
# --- Sparse Bundle (Optional) ---
BUNDLE_PATH="/Volumes/$SMB_SHARE/my_secure_disk.sparsebundle"
BUNDLE_VOLUME_NAME="MyPrivateDisk"
Direct Configuration (Alternative)
You can also edit mount_manager.sh directly, but be aware that updates may overwrite your changes.
nano ~/Scripts/mount_manager.sh
Locate the DEFAULT CONFIGURATION section to adjust settings.
Rclone Advanced Configuration
Rclone mount flags are defined in the RCLONE_MOUNT_ARGS array. You can add, remove, or modify flags in this array to customize the mount behavior.
# In mount_manager.sh:
RCLONE_MOUNT_ARGS=(
"--volname" "CloudStorage"
"--vfs-cache-mode" "full"
"--vfs-cache-max-size" "20G"
"--no-modtime"
# Add custom flags here
)
SMB Share Settings
SMB_ENABLED="true"
SMB_IP="192.168.1.100" # Your NAS/Server IP
SMB_USER="your_username" # SMB username
SMB_SHARE="SharedFolder" # Share name
Rclone Settings
RCLONE_ENABLED="true"
RCLONE_REMOTE="myremote:/path" # Your rclone remote
RCLONE_MOUNT_POINT="$HOME/Mounts/Cloud"
RCLONE_IP="100.x.x.x" # IP to ping (use Tailscale IP for remote)
Sparse Bundle (Optional)
For mounting disk images stored on the SMB share:
BUNDLE_PATH="/Volumes/$SMB_SHARE/Storage.sparsebundle"
BUNDLE_VOLUME_NAME="ExternalStorage"
Auto-Update
v3 replaces the unsafe single-file background updater. Legacy AUTO_UPDATE_ENABLED is ignored. Update your full checkout, then run:
git pull --ff-only
./install.sh update
The installer backs up the old version, preserves configuration, migrates and restarts the service.
Remote Access with Tailscale
LazyMount works beautifully with Tailscale for accessing your home storage from anywhere.
Setup Overview
┌─────────────────────────────────────────────────────────────┐
│ YOUR HOME NETWORK │
│ │
│ ┌───────────────┐ ┌───────────┐ ┌───────────┐ │
│ │ Tailscale │───────▶│ NAS │ │ Server │ │
│ │ Subnet Router │ │ (SMB/AFP) │ │ (SSH/Web) │ │
│ └───────────────┘ └───────────┘ └───────────┘ │
│ │ ▲ │
│ └───────────────────────────────────────┘ │
└──────────────────────────────▲──────────────────────────────┘
│
Tailscale VPN Tunnel
│
┌──────────────────────────────┴──────────────────────────────┐
│ ANYWHERE IN THE WORLD │
│ ┌─────────────────────────┐ │
│ │ Your MacBook │ │
│ │ (LazyMount Client) │ │
│ └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Exit Node Configuration (Subnet Routing)
The magic feature you’re looking for is called “Subnet Router” or “Exit Node” in Tailscale:
-
On your home server (Linux example):
# Enable IP forwarding
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
# Advertise your home subnet
sudo tailscale up --advertise-routes=192.168.1.0/24
-
In Tailscale Admin Console (https://login.tailscale.com/admin):
- Go to Machines → Your server → Enable “Subnet routes”
- Approve the
192.168.1.0/24 route
-
On your Mac (the client):
# Accept the advertised routes
sudo tailscale up --accept-routes
Now your Mac can access 192.168.1.x addresses even when you’re at a coffee shop! 🎉
Use Case Examples
1. Steam Game Library on NAS
Store games on a NAS to save SSD space:
# In mount_manager.sh:
SMB_IP="192.168.1.50" # NAS IP
SMB_USER="steam"
SMB_SHARE="Games" # Share containing Steam library
# Optional: Use sparse bundle for better performance
BUNDLE_PATH="/Volumes/Games/SteamLibrary.sparsebundle"
BUNDLE_VOLUME_NAME="SteamLibrary"
⚠️ Gaming Note:
- Steam/Epic games require an APFS Sparse Bundle to work correctly.
- League of Legends (LOL) does not support running from network drives (even inside APFS).
Then in Steam: Settings → Storage → Add Library Folder → /Volumes/SteamLibrary
2. Time Machine to Remote Server
Back up your Mac to a server over the network:
SMB_IP="192.168.1.10"
SMB_USER="timemachine"
SMB_SHARE="Backups"
BUNDLE_PATH="/Volumes/Backups/MyMac.sparsebundle"
BUNDLE_VOLUME_NAME="TimeMachine"
Then: System Settings → Time Machine → Select Disk → Choose “TimeMachine”
Access your movie library stored on a home server:
RCLONE_ENABLED="true"
RCLONE_REMOTE="homeserver:/media"
RCLONE_MOUNT_POINT="$HOME/Movies/Server"
RCLONE_IP="100.64.0.1" # Tailscale IP of your server
4. Google Drive Dropbox as Local Folder
Mount cloud storage as if it were a local drive:
# First, configure rclone:
# rclone config → New remote → "google" → Google Drive
RCLONE_REMOTE="google:/MyDrive"
RCLONE_MOUNT_POINT="$HOME/GoogleDrive"
RCLONE_IP="8.8.8.8" # Use Google DNS to check internet
5. Work Project Archives
Keep large project files on office NAS, access from home:
SMB_ENABLED="true"
SMB_IP="10.0.0.50" # Office NAS (via VPN/Tailscale)
SMB_USER="employee"
SMB_SHARE="Projects"
6. AI LLM Model Storage
Store large language models (LLaMA, Mistral, Qwen, etc.) on a server instead of your Mac’s limited SSD:
RCLONE_ENABLED="true"
RCLONE_REMOTE="homeserver:/ai-models"
RCLONE_MOUNT_POINT="$HOME/.ollama/models" # Ollama's model directory
RCLONE_IP="192.168.1.10"
⚠️ Important: Network Speed Matters!
LLM models need to be loaded into RAM before inference. If your model isn’t in local cache, it must be transferred over the network. A 70B model can be 40GB+, so network speed is crucial:
| Network Type |
Speed |
Time to Load 40GB Model |
| 1 Gigabit (1G) |
~120 MB/s |
~5.5 minutes |
| 2.5 Gigabit (2.5G) |
~300 MB/s |
~2.2 minutes ✅ Recommended (Mac Mini default) |
| 10 Gigabit (10G) |
~1.2 GB/s |
~33 seconds |
Hardware Recommendations:
┌─────────────────────────────────────────────────────────────────┐
│ 💡 For Best LLM Experience, Upgrade Your Network! │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Option 1: 2.5G USB Adapter (~$15-30) │
│ ┌─────────┐ ┌─────────────────┐ ┌─────────────┐ │
│ │ Mac │─USB─▶│ 2.5G USB Adapter │─────▶│ 2.5G Switch │ │
│ └─────────┘ └─────────────────┘ └─────────────┘ │
│ │
│ Option 2: 10G Thunderbolt Adapter (~$100-200) │
│ ┌─────────┐ ┌───────────────────┐ ┌─────────────┐ │
│ │ Mac │─TB4─▶│ 10G TB Adapter │───▶│ 10G Switch │ │
│ └─────────┘ └───────────────────┘ └─────────────┘ │
│ │
│ ⚠️ Both sides (Mac + Server) must support the speed! │
└─────────────────────────────────────────────────────────────────┘
Cache Settings for LLM (minimize re-downloads):
# In mount_manager.sh, adjust Rclone settings:
--vfs-cache-max-size 100G # Large cache for model files
--vfs-cache-max-age 720h # Keep cached for 30 days
--vfs-read-ahead 1G # Pre-fetch for faster loads
Why this matters:
- LLM apps (Ollama, LM Studio) unload models after idle time (typically 5 minutes)
- Next query requires reloading the full model from network
- Fast network = quick model loading = better experience
Ollama Service Setup (Optional)
If you want Ollama to start automatically at boot and serve models from your network drive (0.0.0.0), use the provided plist:
-
Edit the plist:
Open com.ollama.startup.plist and change /Volumes/ModelStorage/ai-models to your actual mount path (e.g., /Volumes/ModelStorage/ai-models).
-
Install:
cp com.ollama.startup.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.ollama.startup.plist
-
Verify:
Ollama will now start largely and listen on all interfaces. Access it from other devices via http://YOUR_MAC_IP:11434.
Detailed Beginner’s Guide
New to terminal/command line? This section walks you through everything step-by-step.
Step 1: Install Homebrew (Package Manager)
Homebrew is like an “App Store” for command-line tools. If you don’t have it:
# Open Terminal (Spotlight → type "Terminal" → Enter)
# Paste this command and press Enter:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Follow the on-screen instructions
# When done, verify with:
brew --version
# Install Rclone (for cloud/remote mounting)
brew install rclone
# Install FUSE-T (Recommended for M-Series Macs)
# It's newer, faster, and safer (no reboot required!)
brew tap macos-fuse-t/homebrew-cask
brew install fuse-t
# --- OR ---
# Install macFUSE (Legacy, for Intel or specific needs)
# ⚠️ Apple Silicon users will need to enable Kernel Extensions in Recovery Mode
brew install --cask macfuse
If you installed FUSE-T:
You are done! No restarts or security changes needed.
If you installed macFUSE:
- Go to System Settings → Privacy & Security
- Click “Allow” for the system extension
- Restart your Mac
# Start the configuration wizard
rclone config
# Example: Setting up SFTP connection to your server
# n) New remote
# name> homeserver
# Storage> sftp
# host> 192.168.1.10
# user> your_username
# (follow prompts for SSH key or password)
Common remote types:
| Type |
Use Case |
Command |
| SFTP |
Linux servers, NAS with SSH |
rclone config → sftp |
| Google Drive |
Google cloud files |
rclone config → drive |
| Dropbox |
Dropbox files |
rclone config → dropbox |
| S3 |
AWS/MinIO storage |
rclone config → s3 |
Step 4: Test Your Remote
# List files on your remote (replace 'homeserver' with your remote name)
rclone ls homeserver:/
# If you see your files, it's working!
# From a full LazyMount-Mac checkout:
./install.sh install
open -e ~/Scripts/mount_manager.local.sh
Set these values in your local configuration:
# === RCLONE SETTINGS ===
RCLONE_ENABLED="true" # Enable Rclone? (true/false)
RCLONE_REMOTE="homeserver:/data" # ← Your remote name and path
RCLONE_MOUNT_POINT="$HOME/Mounts/Server" # ← Where to mount on your Mac
RCLONE_IP="192.168.1.10" # ← IP to ping for network check
# === SMB SETTINGS ===
SMB_ENABLED="true" # Enable SMB? (true/false)
SMB_IP="192.168.1.100" # ← Your NAS/Server IP
SMB_USER="your_username" # ← Your SMB username
SMB_SHARE="SharedFolder" # ← Share folder name
Step 6: Save SMB Password to Keychain
This is important! The script needs your password stored in Keychain:
- Open Finder → Press
⌘ + K (Connect to Server)
- Type:
smb://[email protected]/SharedFolder
- Enter your password
- ✅ Check “Remember this password in my keychain”
- Click Connect
The script will now connect using the stored credentials.
Step 7: Test the Script Manually
# Run the script to see if it works
~/Scripts/mount_manager.sh
# Watch the log in real-time (open a new terminal window)
tail -f ~/Library/Logs/LazyMount/manager.log
# You should see:
# === Mount Session Started: ... ===
# HH:MM:SS [SMB] Starting sequence...
# HH:MM:SS [SMB] Network OK.
# ...
Step 8: Set Up Auto-Start at Login
./install.sh install
./install.sh status
Step 9: Verify Everything Works
# Check if your volumes are mounted:
# For SMB:
ls /Volumes/
# For Rclone:
ls ~/Mounts/
# View recent logs:
tail -20 ~/Library/Logs/LazyMount/manager.log
Setup Complete. Your storage will now auto-mount every time you log in.
Management Commands
./install.sh status
./install.sh restart
./install.sh update
./install.sh uninstall
tail -f ~/Library/Logs/LazyMount/manager.log
Uninstall stops and removes the LaunchAgent, retaining settings, credentials and data.
❓ FAQ / Troubleshooting
Q: Mount fails with “permission denied”
A: Ensure your SMB credentials are saved in Keychain:
- Open Finder → Go → Connect to Server (⌘K)
- Enter your SMB URL:
smb://username@server/share
- Check “Remember this password in my keychain”
Q: Rclone mount is slow
A: Adjust cache settings in the script:
--vfs-cache-max-size 50G # Increase cache size
--dir-cache-time 5m # Longer directory cache
A: This is normal for Rclone. Reduce --dir-cache-time to 10s for faster refresh.
Q: How do I unmount manually?
# Rclone
diskutil unmount ~/Mounts/CloudStorage
Known Issues
v3 creates a dedicated 4 KiB file, writes, fsyncs, reads back and removes it. An already-mounted unwritable volume enters Degraded; neither df nor writable mount flags prove health. Permission, capacity, read-only, I/O, sync and timeout failures are distinguished. A sync failure alone is not evidence of corruption. Normal mounts do not run First Aid; recovery has a one-hour default cooldown.
Recovery checks open handles and only performs normal unmount/reattach. It never force-ejects or kills games. Plists link the image path, physical stores, container and volume UUID; device IDs are resolved again before operations. Before filesystem repair, confirm a recoverable backup and set BUNDLE_REPAIR_APPROVED_UUID to that volume UUID. Without approval, only normal reattach is attempted. Nonzero repair status stays Degraded.
The old claim that all APFS-over-SMB write failures are harmless sync limitations is incorrect. Investigate ENOTSUP at its actual failing stage. v3.0.2 no longer forces image ownership on attach: the affected network image contained a root-owned filesystem root, so forcing ownership caused EACCES (13) on new file creation despite writable game subdirectories. Omitting the override restored macOS’s disabled-ownership policy and the real APFS root passed write/fsync/readback after an automatic DiskImages2 mount. Ownership is a filesystem security setting; do not change ownership/ACLs globally just to bypass EACCES. See the APFS recovery guide for command semantics and recovery limits.
Advanced Storage Management
AppPorts — External drives save the world!
A perfect companion for LazyMount. While LazyMount handles the connection, AppPorts handles the applications.
- App Slimming: One-click migration of multi-gigabyte applications (Logic Pro, Xcode, Games) to your external drives.
- Seamless Linking: Creates “App Portals” so macOS treats apps as if they are still local.
- Safety First: Optimized for macOS directory structure, with one-click restore anytime.
License
MIT License - See LICENSE for details.
Contributing
Contributions welcome! Please feel free to submit pull requests.
Made with ❤️ for Mac users who refuse to pay Apple’s storage tax.
v3.0.2 image backend
LazyMount detects diskutil image attach support and prefers DiskImages2. Only systems without this capability use legacy hdiutil. A failed modern attachment never falls back to the old driver. On the affected NAS, legacy DiskImages band F_FULLSYNC returned ENOTSUP; a disposable image using DiskImages2 passed write/fsync/readback and normal eject. This is a backend-specific observation, not a claim about every SMB server.
评论