A Guide to Running Custom Scripts Before macOS Apps Launch
On macOS, many developers and power users want to run some pre-launch scripts (such as clearing caches, switching network proxies, syncing configs, or resetting the environment) before starting an app like Navicat, Docker, or an IDE. This document records in detail the complete approach to **running custom scripts automatically before launch** on macOS, along with the core principles and a guide to avoiding permission and code-signing pitfalls.
1. How macOS Launches an App
A macOS .app is essentially a folder with a specific structure (a bundle):
/Applications/AppName.app
└── Contents
├── Info.plist <-- defines CFBundleExecutable (the launch entry point name)
├── MacOS
│ └── AppName <-- the real executable entry file
└── Resources
When you click the icon in Finder, Launchpad, or the Dock, or run open -a AppName in the terminal, the underlying LaunchServices service reads Contents/Info.plist and executes the corresponding executable under Contents/MacOS/.
The core idea: rename the original binary (for example to
AppName_real), then replace the original executable with a shell script of the same name. The shell script first runs your custom logic, and finally usesexecto launch the real program.
2. Prerequisites: Getting Past the “Operation not permitted” Permission Block
When modifying an app inside /Applications, even with sudo you will often hit Operation not permitted. This is because macOS’s TCC (Transparency, Consent, and Control) privacy mechanism has higher priority than root/sudo.
Grant Full Disk Access (required):
- Open System Settings.
- Go to Privacy & Security -> Full Disk Access.
- Find and check the terminal you are using (such as Terminal, iTerm2, or VS Code).
- ⚠️ Critical step: press
Command + Qto fully quit the terminal and reopen it; only then does the permission take effect.
3. The Main Approach: Transparent App-Entry Wrapper (Recommended)
Take Navicat Premium as an example; for other apps just substitute the corresponding app path and filename.
Step 1: Rename the Original Binary
Go into the app’s binary directory and rename the original executable as a backup:
cd "/Applications/Navicat Premium.app/Contents/MacOS"
# Remove any file locks that may exist
sudo chflags -R noschg,nouchg,nodump "/Applications/Navicat Premium.app"
# Rename the original main program to Navicat Premium_real
sudo mv "Navicat Premium" "Navicat Premium_real"
Step 2: Create a Same-Name Shell Wrapper Script
Create a script file named Navicat Premium, the same as the original binary:
cat << 'EOF' | sudo tee "/Applications/Navicat Premium.app/Contents/MacOS/Navicat Premium" > /dev/null
#!/bin/bash
# ==========================================
# 1. Put your custom pre-launch script logic here
# ==========================================
# Example: log the launch, clear a temp directory, run other scripts, and so on
echo "Navicat pre-launch hook executed at $(date)" >> /tmp/navicat_launch.log
# If you have an external script, you can call it here too, for example:
# bash /Users/guojc/scripts/my_pre_launch.sh
# ==========================================
# 2. Launch the real Navicat binary
# ==========================================
DIR="$(cd "$(dirname "$0")" && pwd)"
exec "$DIR/Navicat Premium_real" "$@"
EOF
Step 3: Make the Script Executable
sudo chmod +x "/Applications/Navicat Premium.app/Contents/MacOS/Navicat Premium"
Step 4: Fix Gatekeeper Quarantine and Code Signing
Because you modified the contents of the .app bundle, the original developer signature is broken, which may cause macOS to say the app is “damaged” or to refuse to launch it. You need to clear the extended attributes and re-sign it locally with an ad-hoc signature:
# 1. Remove the macOS Quarantine attribute
sudo xattr -c -r "/Applications/Navicat Premium.app"
# 2. Re-sign the app locally
sudo codesign --force --deep --sign - "/Applications/Navicat Premium.app"
After completing the steps above, whether you launch the app from Launchpad, the Dock, or the command line, your custom script runs first automatically.
4. Alternative: A Standalone Automator Launcher
If you would rather not touch the inside of the .app, you can use macOS’s built-in Automator:
- Open Automator -> New Document -> choose Application.
- In the left panel, search for and add Run Shell Script.
- Fill in the script logic:
# 1. Custom pre-launch logic echo "Pre-launch..." # 2. Open the original App open -a "Navicat Premium" - Export and save it as
启动Navicat.app, then place it in/Applicationsor the Dock and launch it from there.
5. Common Issues and Troubleshooting (FAQ)
Q1: chmod or xattr reports [Errno 1] Operation not permitted?
- Cause: The terminal app currently does not have macOS Full Disk Access.
- Fix: Add your terminal under “System Settings -> Privacy & Security -> Full Disk Access” and check it, then quit the terminal (Cmd+Q) and reopen it.
Q2: On launch it says “The application is damaged and can’t be opened”?
- Cause: Modifying files inside the bundle made code-signature validation fail.
- Fix: Run the following repair commands:
sudo xattr -r -d com.apple.quarantine "/Applications/Navicat Premium.app" sudo codesign --force --deep --sign - "/Applications/Navicat Premium.app"
Q3: The script stopped working after a software update?
- Cause: When the app updates by overwriting or auto-updating, it pulls down a complete fresh
.appbundle, which overwrites your wrapper script. - Fix: Just repeat Steps 1 ~ 4 above once more after the upgrade.
Comments