diff --git a/.clang-tidy b/.clang-tidy
index b914e66b2dbd60a60b3c86493b0f09439fe15064..25c56ca8ff50086dfac3a670170c396ad6327874 100644
--- a/.clang-tidy
+++ b/.clang-tidy
@@ -21,4 +21,4 @@ WarningsAsErrors: >
   readability-*,
   performance-*,
 
-HeaderFilterRegex: .*
+HeaderFilterRegex: '(?!subprojects).*'
diff --git a/Makefile b/Makefile
index 5081a62e7be743da695d472c71ac761eb2d69953..f3e0357dbf1bf9f9a7c9391bf585d13886a6db36 100644
--- a/Makefile
+++ b/Makefile
@@ -88,13 +88,18 @@ format: all
 	$(NINJA) -C build clang-format
 
 .PHONY: tidy
-tidy: all
-	$(NINJA) -C build clang-tidy
+tidy: compile_commands_wo_subprojects/compile_commands.json
+	./tools/run-clang-tidy
 
 PHONY: iwyu
-iwyu: all
+iwyu: compile_commands_wo_subprojects/compile_commands.json
 	$(NINJA) -C build $@
 
+build/compile_commands.json: all
+
+compile_commands_wo_subprojects/compile_commands.json: all build/compile_commands.json
+	./tools/gen-compile-commands-wo-subprojects
+
 PHONY: fix-includes
 fix-includes: all
 	./tools/fix-includes
diff --git a/compile_commands_wo_subprojects/.gitignore b/compile_commands_wo_subprojects/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..cfd3a8cd0688a8dbf862f6562d41a5ebdc290f03
--- /dev/null
+++ b/compile_commands_wo_subprojects/.gitignore
@@ -0,0 +1 @@
+/compile_commands.json
diff --git a/tools/check-iwyu b/tools/check-iwyu
index 67b431eae9156bc3737c628d063eda80fbbe71bf..e303e91f42100f06faf50a698c890912cb7ed0c8 100755
--- a/tools/check-iwyu
+++ b/tools/check-iwyu
@@ -53,7 +53,7 @@ NPROC=$(nproc)
 LOAD=$(python -c "print(${NPROC} * 1.5)")
 
 IWYU_TOOL_ARGS=(
-	-p "${MESON_BUILD_ROOT}"
+	-p "${MESON_SOURCE_ROOT}/compile_commands_wo_subprojects"
 	--jobs "${NPROC}"
 )
 
diff --git a/tools/gen-compile-commands-wo-subprojects b/tools/gen-compile-commands-wo-subprojects
new file mode 100755
index 0000000000000000000000000000000000000000..924ae107bf9a8343e7cbbef601309cd66d7c4838
--- /dev/null
+++ b/tools/gen-compile-commands-wo-subprojects
@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+
+from pathlib import Path
+
+import json
+
+COMPILE_COMMANDS_FILENAME = "compile_commands.json"
+
+input_compile_db_path = Path(COMPILE_COMMANDS_FILENAME)
+
+with input_compile_db_path.open() as f:
+    input_compile_db = json.load(f)
+
+output_compile_db = []
+
+for entry in input_compile_db:
+    entry_file = entry["file"]
+    if entry_file.startswith("../subprojects/"):
+        continue
+
+    output_compile_db.append(entry)
+
+output_compile_db_path = Path("compile_commands_wo_subprojects") / Path(COMPILE_COMMANDS_FILENAME)
+
+with output_compile_db_path.open(mode='w') as f:
+    json.dump(output_compile_db, f, indent=4)
diff --git a/tools/run-clang-tidy b/tools/run-clang-tidy
new file mode 100755
index 0000000000000000000000000000000000000000..8a60ee29091b0928d9e4a5d59ff897843081b8aa
--- /dev/null
+++ b/tools/run-clang-tidy
@@ -0,0 +1,42 @@
+#!/usr/bin/env bash
+
+SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
+ROOTDIR="$(realpath "${SCRIPTDIR}/..")"
+
+while getopts dv OPT; do
+	case $OPT in
+		d)
+			set -x
+			;;
+
+		*)
+			echo "usage: ${0##*/} [-d]"
+			exit 2
+	esac
+done
+shift $(( OPTIND - 1 ))
+OPTIND=1
+
+RUN_CLANG_TIDY_CANDIDATES=(
+	run-clang-tidy
+	run-clang-tidy.py
+	/usr/share/clang/run-clang-tidy.py
+)
+
+RUN_CLANG_TIDY=""
+
+for candidate in ${RUN_CLANG_TIDY_CANDIDATES[@]}; do
+	if ! command -v "${candidate}"; then
+		continue;
+	fi
+
+	RUN_CLANG_TIDY="${candidate}"
+	break;
+done
+
+if [[ -z "${RUN_CLANG_TIDY}" ]]; then
+	echo "No run-clang-tidy executable found"
+	exit 1
+fi
+
+${RUN_CLANG_TIDY} -p "${ROOTDIR}/compile_commands_wo_subprojects/"