diff --git a/utils/libcamera-bug-report b/utils/libcamera-bug-report
new file mode 100755
index 000000000000..6c7d1d86557a
--- /dev/null
+++ b/utils/libcamera-bug-report
@@ -0,0 +1,119 @@
+#!/bin/sh
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+REPORT="libcamera-report-$(hostname)-$(date +%Y%m%d-%H%M%S).txt"
+
+section() {
+    echo
+    echo "=================================================="
+    echo "$1"
+    echo "=================================================="
+}
+
+run() {
+    echo
+    echo "\$ $*"
+    "$@" || echo "[WARN] Command failed: $*"
+}
+
+# Run a command with sudo if the script was allowed to use it.
+run_with_sudo() {
+    if [ -n "$SUDO" ]; then
+        echo
+        echo "\$ $SUDO $*"
+        $SUDO "$@" || echo "[WARN] Command failed: $SUDO $*"
+    else
+        run "$@"
+    fi
+}
+
+mark_kmsg() {
+    if [ -w /dev/kmsg ]; then
+        echo "libcamera-debug-report[$$]: $1" > /dev/kmsg
+    else
+        if [ -n "$SUDO" ]; then
+            printf "libcamera-debug-report[%s]: %s\n" "$$" "$1" | $SUDO tee /dev/kmsg >/dev/null || \
+                echo "[WARN] failed to write /dev/kmsg with sudo: $1"
+        else
+            echo "[INFO] /dev/kmsg not writable, skipping kernel marker: $1"
+        fi
+    fi
+}
+
+main() {
+
+section "Report metadata"
+echo "Date: $(date -Is)"
+echo "Host: $(hostname)"
+echo "User: $(id)"
+
+section "Kernel & OS"
+run uname -a
+run cat /etc/os-release
+command -v lsb_release >/dev/null && run lsb_release -a
+
+section "Media / V4L2 tools"
+command -v media-ctl >/dev/null && run media-ctl --version
+command -v v4l2-ctl >/dev/null && run v4l2-ctl --version
+command -v v4l2-ctl >/dev/null && run v4l2-ctl --list-devices
+
+section "Device nodes"
+run ls -l /dev/video* /dev/v4l-* /dev/media* 2>/dev/null
+run grep . /sys/class/video4linux/*/name
+
+section "Deferred devices"
+run cat /sys/kernel/debug/devices_deferred
+
+section "V4L2 async pending subdevices"
+run_with_sudo cat /sys/kernel/debug/v4l2-async/pending_async_subdevices
+
+section "Media graph topology"
+for m in /dev/media*; do
+    echo
+    echo "Parsing $m"
+    command -v media-ctl >/dev/null && run media-ctl -p "$m"
+done
+
+section "libcamera & userspace"
+run which cam
+command -v cam >/dev/null && run ldd "$(which cam)"
+
+section "libcamera probe (cam -l)"
+mark_kmsg "BEGIN cam -l"
+LIBCAMERA_LOG_LEVELS="*:0" run cam -l
+mark_kmsg "END cam -l"
+
+section "Kernel log (post cam -l)"
+run_with_sudo dmesg
+
+section "Permissions & capabilities"
+run id
+run groups
+command -v getcap >/dev/null && run getcap "$(which cam)"
+
+section "End of report"
+
+cat <<EOF
+
+Report saved to $REPORT.
+
+Please inspect the report and remove any sensitive information before sharing.
+EOF
+
+}
+
+# If not running as root, prompt the user once to allow sudo
+SUDO=""
+if [ "$(id -u)" -ne 0 ]; then
+    if [ -t 0 ]; then
+        printf "Some checks require root; allow sudo for privileged commands? [y/N] "
+        read -r ans
+        case "$ans" in
+            [Yy]|[Yy][Ee][Ss]) SUDO="sudo" ;;
+            *) SUDO="" ;;
+        esac
+    fi
+fi
+
+# Run main and tee output to both stdout and the report file.
+main 2>&1 | tee "$REPORT"
diff --git a/utils/meson.build b/utils/meson.build
index 3deed8ad4d7e..9c598793035c 100644
--- a/utils/meson.build
+++ b/utils/meson.build
@@ -7,3 +7,9 @@ gen_shader_headers = files('gen-shader-headers.sh')
 
 ## Module signing
 gen_ipa_priv_key = files('gen-ipa-priv-key.sh')
+
+## Bug reporting utility
+install_data(
+    'libcamera-bug-report',
+    install_dir: get_option('bindir'),
+)
