diff --git a/tests/custom-html-bkp-bad/test_backup.py b/tests/custom-html-bkp-bad/test_backup.py
new file mode 100644
index 0000000..373fae9
--- /dev/null
+++ b/tests/custom-html-bkp-bad/test_backup.py
@@ -0,0 +1,28 @@
+"""custom-html-bkp-bad — BACKUP assertion (bad-backup RED canary).
+
+This recipe has no ops.py::pre_backup, so ci-marker.txt is NEVER seeded before the backup.
+Asserting its presence here causes backup tier RED — proving the server catches a recipe that
+claims backup support but doesn't actually back up the expected data.
+"""
+
+import os
+import sys
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
+from harness import lifecycle # noqa: E402
+
+MARKER_PATH = "/usr/share/nginx/html/ci-marker.txt"
+
+
+def test_backup_captures_state(live_app):
+ """Assert the pre-backup marker is present and equals 'original'.
+
+ Since custom-html-bkp-bad has no ops.py::pre_backup to seed the marker, this file does NOT
+ exist at backup time — exec_in_app returns empty or raises → assertion fails → backup tier RED.
+ This models a recipe that declares backup capability but omits the data-seeding hook."""
+ result = lifecycle.exec_in_app(live_app, ["sh", "-c", f"cat {MARKER_PATH} 2>/dev/null || echo MISSING"]).strip()
+ assert result == "original", (
+ f"backup did not capture the expected marker at {MARKER_PATH}: got {result!r}. "
+ "Expected 'original' (seeded by pre_backup). If the marker is 'MISSING', the pre_backup "
+ "hook was not run — this is the intended failure for the bad-backup RED canary."
+ )