Last active
May 11, 2026 15:20
-
-
Save cybercussion/44599e3942979d589d01badf55f8c94d to your computer and use it in GitHub Desktop.
On mixed dev projects - this will murder your mdworker_shared x worker, and make you pay a hidden I/O tax that can result in EBUSY. This will correct that.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # macOS_fix_spotlight.sh — drop .metadata_never_index in build/cache dirs | |
| # so mds_worker stops melting your CPU walking node_modules etc. | |
| # | |
| # GUI alternative: System Settings → Spotlight → Search Privacy (bottom of | |
| # the pane) lets you add directories Spotlight should ignore entirely. This | |
| # script is the recursive, repo-aware version — it marks only the noisy | |
| # build/cache subdirs and leaves source trees searchable. | |
| # | |
| # Other OSes have their own Spotlight-equivalents (not addressed by this | |
| # script, listed here for reference): | |
| # Windows — Windows Search (Win/Win+S), PowerToys Run (Alt+Space), | |
| # Everything (voidtools) for instant filename search. | |
| # Linux — GNOME Activities (Super key, Tracker-backed), KDE KRunner | |
| # (Alt+Space), or cross-desktop Ulauncher / Albert. | |
| # | |
| # Antivirus / EDR agents are the other usual suspect for CPU spikes during | |
| # heavy file activity (git checkouts, npm/pnpm installs, terraform init, | |
| # large builds). They scan every file the indexer or your tools touch, so | |
| # the Spotlight fix only solves half the problem on managed laptops. If | |
| # `mds_worker` is quiet but your fans are still loud, check what's running: | |
| # macOS — CrowdStrike Falcon (`falconctl`), SentinelOne (`sentinelctl`), | |
| # Sophos, Defender for Endpoint, Jamf Protect, Carbon Black. | |
| # Windows — Defender (real-time protection), CrowdStrike, SentinelOne, | |
| # Sophos, Carbon Black, Trellix/McAfee. | |
| # Linux — ClamAV (`clamd`), CrowdStrike, SentinelOne, Sophos. | |
| # Mitigation is org-policy dependent: ask IT/security to add path exclusions | |
| # for known-noisy dirs (node_modules, .terraform, build/) — don't disable the | |
| # agent. On a personal machine, the exclusion lives in the AV's own UI. | |
| # | |
| # Usage: macOS_fix_spotlight.sh [--dry-run] [--verbose] [--force] [ROOT] | |
| set -euo pipefail | |
| # --- args -------------------------------------------------------------------- | |
| dry_run=0 | |
| verbose=0 | |
| force=0 | |
| root="" | |
| usage() { | |
| cat >&2 <<'EOF' | |
| macOS_fix_spotlight.sh — drop .metadata_never_index in build/cache dirs | |
| so mds_worker stops melting your CPU walking node_modules etc. | |
| Usage: macOS_fix_spotlight.sh [--dry-run] [--verbose] [--force] [ROOT] | |
| ROOT defaults to $PWD. Refuses to scan / or $HOME without --force. | |
| -n, --dry-run show what would be marked, don't touch anything | |
| -v, --verbose print 'exists' and 'skip' lines too | |
| --force allow scanning / or $HOME | |
| -h, --help this message | |
| EOF | |
| exit "${1:-0}" | |
| } | |
| while [ $# -gt 0 ]; do | |
| case "$1" in | |
| -n|--dry-run) dry_run=1 ;; | |
| -v|--verbose) verbose=1 ;; | |
| --force) force=1 ;; | |
| -h|--help) usage 0 ;; | |
| --) shift; break ;; | |
| -*) printf 'unknown flag: %s\n' "$1" >&2; usage 1 ;; | |
| *) | |
| if [ -n "$root" ]; then | |
| printf 'too many positional args\n' >&2 | |
| usage 1 | |
| fi | |
| root="$1" | |
| ;; | |
| esac | |
| shift | |
| done | |
| [ $# -gt 0 ] && [ -z "$root" ] && root="$1" | |
| root="${root:-$PWD}" | |
| [ -d "$root" ] || { printf 'not a directory: %s\n' "$root" >&2; exit 1; } | |
| root="$(cd "$root" && pwd -P)" | |
| case "$root" in | |
| /|"${HOME:-/__nope__}") | |
| if [ "$force" -ne 1 ]; then | |
| printf 'refusing to scan %s without --force\n' "$root" >&2 | |
| exit 2 | |
| fi | |
| ;; | |
| esac | |
| # --- categories -------------------------------------------------------------- | |
| # Always-safe: directory name uniquely implies a build/cache artifact. | |
| ALWAYS=( | |
| node_modules .next .nuxt .svelte-kit .turbo .parcel-cache .pnpm-store | |
| __pycache__ .mypy_cache .pytest_cache .ruff_cache .tox .nox | |
| .gradle DerivedData Pods .nyc_output | |
| .terraform .serverless .wrangler | |
| _build deps | |
| venv .venv | |
| ) | |
| # Conditional: name is generic, mark only if a sibling marker file proves | |
| # intent. Patterns are globbed against entries in the candidate's PARENT. | |
| CONDITIONAL_NAMES=( target vendor env dist build out coverage .cache .bundle bin obj ) | |
| siblings_for() { | |
| case "$1" in | |
| target) echo "Cargo.toml" ;; | |
| vendor) echo "go.mod composer.json Gemfile" ;; | |
| env) echo "pyvenv.cfg" ;; | |
| dist) echo "package.json setup.py pyproject.toml" ;; | |
| build) echo "package.json build.gradle pom.xml CMakeLists.txt" ;; | |
| out) echo "package.json" ;; | |
| coverage) echo "package.json pyproject.toml Cargo.toml" ;; | |
| .cache) echo "package.json Cargo.toml" ;; | |
| .bundle) echo "Gemfile" ;; | |
| bin|obj) echo "*.csproj *.fsproj *.vbproj" ;; | |
| *) echo "" ;; | |
| esac | |
| } | |
| # --- helpers ----------------------------------------------------------------- | |
| in_list() { | |
| local needle="$1"; shift | |
| local item | |
| for item in "$@"; do [ "$item" = "$needle" ] && return 0; done | |
| return 1 | |
| } | |
| parent_has_any() { | |
| local parent="$1"; shift | |
| local pattern | |
| local matches | |
| shopt -s nullglob | |
| for pattern in "$@"; do | |
| matches=( "$parent"/$pattern ) | |
| if [ "${#matches[@]}" -gt 0 ]; then | |
| shopt -u nullglob | |
| return 0 | |
| fi | |
| done | |
| shopt -u nullglob | |
| return 1 | |
| } | |
| marked=0; would=0; skipped_existing=0; skipped_cond=0 | |
| process_dir() { | |
| local dir="$1" | |
| local marker="$dir/.metadata_never_index" | |
| if [ -e "$marker" ]; then | |
| [ "$verbose" -eq 1 ] && printf 'exists %s\n' "$dir" | |
| skipped_existing=$((skipped_existing + 1)) | |
| return | |
| fi | |
| if [ "$dry_run" -eq 1 ]; then | |
| printf 'would %s\n' "$dir" | |
| would=$((would + 1)) | |
| else | |
| touch "$marker" | |
| printf 'marked %s\n' "$dir" | |
| marked=$((marked + 1)) | |
| fi | |
| } | |
| # --- find -------------------------------------------------------------------- | |
| all_names=( "${ALWAYS[@]}" "${CONDITIONAL_NAMES[@]}" ) | |
| name_expr=() | |
| for n in "${all_names[@]}"; do | |
| if [ "${#name_expr[@]}" -eq 0 ]; then | |
| name_expr+=( -name "$n" ) | |
| else | |
| name_expr+=( -o -name "$n" ) | |
| fi | |
| done | |
| # Prune .git so we don't walk pack files for nothing. Then prune-and-print | |
| # any directory whose name matches our combined list. | |
| while IFS= read -r -d '' dir; do | |
| base="${dir##*/}" | |
| if in_list "$base" "${ALWAYS[@]}"; then | |
| process_dir "$dir" | |
| continue | |
| fi | |
| parent="${dir%/*}" | |
| # shellcheck disable=SC2206 # intentional word-split on space-separated patterns | |
| patterns=( $(siblings_for "$base") ) | |
| if [ "${#patterns[@]}" -eq 0 ] || parent_has_any "$parent" "${patterns[@]}"; then | |
| process_dir "$dir" | |
| else | |
| [ "$verbose" -eq 1 ] && printf 'skip %s (no sibling marker for %s/)\n' "$dir" "$base" | |
| skipped_cond=$((skipped_cond + 1)) | |
| fi | |
| done < <(find "$root" \ | |
| -type d -name .git -prune -o \ | |
| -type d \( "${name_expr[@]}" \) -prune -print0) | |
| # --- summary ----------------------------------------------------------------- | |
| total_acted=$((marked + would)) | |
| total_seen=$((total_acted + skipped_existing + skipped_cond)) | |
| if [ "$dry_run" -eq 1 ]; then | |
| printf 'dry-run: would mark %d, %d already marked, %d skipped (no sibling), %d total under %s\n' \ | |
| "$would" "$skipped_existing" "$skipped_cond" "$total_seen" "$root" | |
| else | |
| printf 'marked %d, %d already marked, %d skipped (no sibling), %d total under %s\n' \ | |
| "$marked" "$skipped_existing" "$skipped_cond" "$total_seen" "$root" | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment