diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index fe8466433ac..d75468d0b48 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -112,6 +112,11 @@ repos:
       additional_dependencies: ["pyyaml"]
       always_run: true
       require_serial: true
+    - id: check-file-mode
+      name: check-file-mode
+      language: script
+      entry: gdb/contrib/check-file-mode.sh
+      files: '^(gdb|gdbserver|gdbsupport)/.*$'
   - repo: https://github.com/nmoroze/tclint
     rev: v0.8.0
     hooks:
diff --git a/gdb/contrib/check-file-mode.sh b/gdb/contrib/check-file-mode.sh
new file mode 100755
index 00000000000..a558731c425
--- /dev/null
+++ b/gdb/contrib/check-file-mode.sh
@@ -0,0 +1,42 @@
+#!/bin/bash
+
+# Copyright (C) 2026 Free Software Foundation, Inc.
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+set -e
+
+no_exec_files=()
+for f in "$@"; do
+    case $f in
+	*/*.py|*/*.sh|*/configure)
+	    continue
+	    ;;
+	gdb/gstack-1.in|gdb/gcore-1.in|gdb/po/gdbtext|gdb/make-init-c|gdb/testsuite/lib/notty-wrap|gdb/testsuite/lib/pdtrace.in)
+	    continue
+	    ;;
+	*)
+	    no_exec_files=("${no_exec_files[@]}" "$f")
+	    ;;
+    esac
+done
+
+if [ ${#no_exec_files[@]} -eq 0 ]; then
+    exit 0
+fi
+
+# Flag files that are executable, but not meant to be executable.
+
+if git ls-files --stage -- "${no_exec_files[@]}" | grep '^100755 '; then
+    exit 1
+fi
