How to Launch VS Code Projects via macOS Spotlight
A while ago, I shared a Bash script for GNOME that generated individual .desktop launchers for my VS Code projects. It was a massive productivity win: I could hit the Super key, type a project name, and jump straight into the code without digging through directories.
I've since moved over to a new Apple MacBook Pro (honestly, perhaps the best machine I've ever owned, but that's a post for another time) and replicating that workflow was my first priority. Since I'm still finding my "macOS legs," I teamed up with Claude to port the logic over.
The goal remains the same, but the implementation is native to macOS. Instead of .desktop files, the script now generates lightweight .app wrappers inside ~/Applications/ProjectLaunchers. This allows Spotlight (Cmd + Space) to index my projects perfectly, giving me the same "search-and-launch" speed I had on Linux.
Here is the script:
#!/usr/bin/env bash
if [ "$(uname -s)" != "Darwin" ]; then
echo "This script requires macOS." >&2
exit 1
fi
PROJECTS_DIR="$HOME/Projects"
APP_DIR="$HOME/Applications/ProjectLaunchers"
# Safety check: ensure APP_DIR is set and not root
if [ -z "$APP_DIR" ] || [ "$APP_DIR" = "/" ]; then
echo "Invalid APP_DIR: '$APP_DIR'" >&2
exit 1
fi
# If directory exists, empty its contents (keep the directory itself).
if [ -d "$APP_DIR" ]; then
echo "Cleaning existing launchers in: $APP_DIR"
# Remove all children of APP_DIR safely
find "$APP_DIR" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
else
mkdir -p "$APP_DIR"
fi
VSCODE_APP="/Applications/Visual Studio Code.app"
VSCODE_ICON="$VSCODE_APP/Contents/Resources/Code.icns"
if [ ! -d "$VSCODE_APP" ]; then
echo "⚠️ Visual Studio Code not found in /Applications. Icons will be generic."
VSCODE_ICON=""
fi
find "$PROJECTS_DIR" -type f -name ".projectname" | while read -r project_file; do
project_dir=$(dirname "$project_file")
project_short_name=$(head -n 1 "$project_file" | tr -d '[:space:]')
[ -z "$project_short_name" ] && continue
APP_PATH="$APP_DIR/$project_short_name.app"
# Prefer building a small AppleScript-based app (avoids creating an
# Intel-only launcher binary which triggers Rosetta prompts on Apple
# Silicon). Fall back to a simple bundle if osacompile isn't present.
if command -v osacompile >/dev/null 2>&1; then
TMP_AS="$TMPDIR/${project_short_name// /_}-$RANDOM.applescript"
cat > "$TMP_AS" <<AS
on run
tell application "Visual Studio Code"
open POSIX file "$project_dir"
activate
end tell
end run
AS
# Compile AppleScript to an app bundle
/usr/bin/osacompile -o "$APP_PATH" "$TMP_AS" >/dev/null 2>&1 || true
rm -f "$TMP_AS"
# If compilation failed, fall back to shell-bundle creation below
if [ ! -d "$APP_PATH" ]; then
echo "Warning: osacompile failed for $project_short_name; falling back to shell launcher." >&2
else
# Copy VS Code icon into the compiled app and update plist
if [ -n "$VSCODE_ICON" ] && [ -f "$VSCODE_ICON" ]; then
mkdir -p "$APP_PATH/Contents/Resources"
cp "$VSCODE_ICON" "$APP_PATH/Contents/Resources/Code.icns"
PLIST="$APP_PATH/Contents/Info.plist"
if [ -f "$PLIST" ] && command -v /usr/libexec/PlistBuddy >/dev/null 2>&1; then
/usr/libexec/PlistBuddy -c "Set :CFBundleName $project_short_name" "$PLIST" 2>/dev/null || /usr/libexec/PlistBuddy -c "Add :CFBundleName string $project_short_name" "$PLIST" 2>/dev/null
/usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName $project_short_name" "$PLIST" 2>/dev/null || /usr/libexec/PlistBuddy -c "Add :CFBundleDisplayName string $project_short_name" "$PLIST" 2>/dev/null
ID="com.user.$(echo "$project_short_name" | tr ' ' '_')"
/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $ID" "$PLIST" 2>/dev/null || /usr/libexec/PlistBuddy -c "Add :CFBundleIdentifier string $ID" "$PLIST" 2>/dev/null
/usr/libexec/PlistBuddy -c "Set :CFBundleIconFile Code" "$PLIST" 2>/dev/null || /usr/libexec/PlistBuddy -c "Add :CFBundleIconFile string Code" "$PLIST" 2>/dev/null
/usr/libexec/PlistBuddy -c "Set :CFBundleIconName Code" "$PLIST" 2>/dev/null || /usr/libexec/PlistBuddy -c "Add :CFBundleIconName string Code" "$PLIST" 2>/dev/null
# Remove the default applet icon if present so the new icon is used
rm -f "$APP_PATH/Contents/Resources/applet.icns" "$APP_PATH/Contents/Resources/applet.rsrc" 2>/dev/null || true
fi
LSREGISTER="/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister"
if [ -x "$LSREGISTER" ]; then
"$LSREGISTER" -f "$APP_PATH" >/dev/null 2>&1 || true
fi
xattr -rd com.apple.quarantine "$APP_PATH" 2>/dev/null || true
fi
echo "Created: $project_short_name.app -> $project_dir"
continue
fi
fi
# Fallback: create a minimal bundle that runs a shell script via the system shell.
CONTENTS="$APP_PATH/Contents"
mkdir -p "$CONTENTS/MacOS" "$CONTENTS/Resources"
EXECUTABLE="$CONTENTS/MacOS/launcher"
cat > "$EXECUTABLE" <<SH
#!/usr/bin/env bash
open -a "Visual Studio Code" -- "$project_dir"
SH
chmod +x "$EXECUTABLE"
cat > "$CONTENTS/Info.plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>$project_short_name</string>
<key>CFBundleDisplayName</key>
<string>$project_short_name</string>
<key>CFBundleIdentifier</key>
<string>com.user.$(echo "$project_short_name" | tr ' ' '_')</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>CFBundleExecutable</key>
<string>launcher</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleIconFile</key>
<string>Code</string>
</dict>
</plist>
PLIST
if [ -n "$VSCODE_ICON" ] && [ -f "$VSCODE_ICON" ]; then
cp "$VSCODE_ICON" "$CONTENTS/Resources/Code.icns"
fi
LSREGISTER="/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister"
if [ -x "$LSREGISTER" ]; then
"$LSREGISTER" -f "$APP_PATH" >/dev/null 2>&1 || true
fi
xattr -rd com.apple.quarantine "$APP_PATH" 2>/dev/null || true
echo "Created: $project_short_name.app -> $project_dir"
done
echo "✅ Done! Find your launchers in: $APP_DIR"