Skip to content

Instantly share code, notes, and snippets.

@cybercussion
Last active May 11, 2026 15:20
Show Gist options
  • Select an option

  • Save cybercussion/44599e3942979d589d01badf55f8c94d to your computer and use it in GitHub Desktop.

Select an option

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.
#!/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