diff --git a/CHANGELOG.md b/CHANGELOG.md
index ee94d87d..fd9576b9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file.
 - Add Channel.last_notify_duration field, use it in "sendalerts" for prioritization
 - Update Telegram integration to treat "bot was blocked by the user" as permanent error
 - Add "Time Zone" field in notifications that use the "Schedule" field (#863)
+- Add bold and monospace text formatting in Signal notifications
 
 ### Bug Fixes
 - Fix "senddeletionnotices" to recognize "Supporter" subscriptions
diff --git a/hc/api/models.py b/hc/api/models.py
index 071747f5..8bac3065 100644
--- a/hc/api/models.py
+++ b/hc/api/models.py
@@ -762,12 +762,13 @@ class Channel(models.Model):
         """
         mail_admins(subject, message, html_message=html_message)
 
-    def send_signal_rate_limited_notice(self, message):
+    def send_signal_rate_limited_notice(self, message: str, plaintext: str):
         email = self.project.owner.email
         ctx = {
             "recipient": self.phone_number,
-            "subject": message.split("\n")[0],
+            "subject": plaintext.split("\n")[0],
             "message": message,
+            "plaintext": plaintext,
         }
         emails.signal_rate_limited(email, ctx)
 
diff --git a/hc/api/tests/test_notify_signal.py b/hc/api/tests/test_notify_signal.py
index b90aee4c..738c384b 100644
--- a/hc/api/tests/test_notify_signal.py
+++ b/hc/api/tests/test_notify_signal.py
@@ -95,7 +95,9 @@ class NotifySignalTestCase(BaseTestCase):
         self.assertEqual(n.error, "")
 
         params = socketobj.req["params"]
-        self.assertIn("“Daily Backup” is DOWN", params["message"])
+        self.assertIn("Daily Backup is DOWN", params["message"])
+        self.assertEqual(params["textStyle"][0], "10:12:BOLD")
+
         self.assertIn("Project: Alices Project", params["message"])
         self.assertIn("Tags: foo, bar", params["message"])
         self.assertIn("Period: 1 day", params["message"])
@@ -138,7 +140,7 @@ class NotifySignalTestCase(BaseTestCase):
         self.assertEqual(n.error, "")
 
         params = socketobj.req["params"]
-        self.assertIn("“Foo & Co” is DOWN", params["message"])
+        self.assertIn("Foo & Co is DOWN", params["message"])
         self.assertIn("Project: Alice & Friends", params["message"])
         self.assertIn("Tags: foo, a&b", params["message"])
 
@@ -328,6 +330,9 @@ class NotifySignalTestCase(BaseTestCase):
         }
         setup_mock(socket, msg)
 
+        self.check.name = "Foo & Co"
+        self.check.save()
+
         self.channel.notify(self.check)
 
         n = Notification.objects.get()
@@ -343,8 +348,15 @@ class NotifySignalTestCase(BaseTestCase):
         email = emails["alice@example.org"]
         self.assertEqual(
             email.subject,
-            "Signal notification failed: The check “Daily Backup” is DOWN.",
+            "Signal notification failed: The check Foo & Co is DOWN.",
         )
+        # The plaintext version should have no HTML markup, and should
+        # have no &, < > stuff:
+        self.assertIn("The check Foo & Co is DOWN.", email.body)
+        # The HTML version should retain styling, and escape special characters
+        # in project name, check name, etc.:
+        html = email.alternatives[0][0]
+        self.assertIn("The check <b>Foo &amp; Co</b> is <b>DOWN</b>.", html)
 
     @patch("hc.api.transports.socket.socket")
     def test_it_handles_null_data(self, socket):
diff --git a/hc/api/transports.py b/hc/api/transports.py
index 7f4035b4..e53d61cb 100644
--- a/hc/api/transports.py
+++ b/hc/api/transports.py
@@ -23,6 +23,7 @@ from hc.front.templatetags.hc_extras import (
 )
 from hc.lib import curl, emails, jsonschema
 from hc.lib.date import format_duration
+from hc.lib.html import extract_signal_styles
 from hc.lib.signing import sign_bounce_id
 from hc.lib.string import replace
 
@@ -1047,10 +1048,15 @@ class Signal(Transport):
             return not self.channel.signal_notify_up
 
     def send(self, recipient: str, message: str) -> None:
+        plaintext, styles = extract_signal_styles(message)
         payload = {
             "jsonrpc": "2.0",
             "method": "send",
-            "params": {"recipient": [recipient], "message": message},
+            "params": {
+                "recipient": [recipient],
+                "message": plaintext,
+                "textStyle": styles,
+            },
             "id": str(uuid.uuid4()),
         }
 
@@ -1077,7 +1083,9 @@ class Signal(Transport):
                         if self.channel:
                             raw = reply_bytes.decode()
                             self.channel.send_signal_captcha_alert(result["token"], raw)
-                            self.channel.send_signal_rate_limited_notice(message)
+                            self.channel.send_signal_rate_limited_notice(
+                                message, plaintext
+                            )
                         raise TransportError("CAPTCHA proof required")
 
                 code = reply["error"].get("code")
diff --git a/hc/lib/html.py b/hc/lib/html.py
index 1fa4c1ca..1607a8a7 100644
--- a/hc/lib/html.py
+++ b/hc/lib/html.py
@@ -1,5 +1,7 @@
 from __future__ import annotations
 
+import re
+from html import unescape
 from html.parser import HTMLParser
 
 
@@ -34,3 +36,42 @@ def html2text(html, skip_pre=False):
 
     parser.feed(html)
     return parser.get_text()
+
+
+def extract_signal_styles(markup: str) -> tuple[str, list[str]]:
+    """Convert HTML syntax to Signal text styles.
+
+    This implementation has limited functionality, and only supports the features
+    we do use:
+    * only supports <b> and <code> tags
+    * does not support nested (<b><code>text</code></b>) tags
+
+    Example:
+
+    >>> extract_signal_styles("<b>foo</b> bar")
+    "foo bar", ["0:3:BOLD"]
+
+    """
+    text = ""
+    styles: list[str] = []
+    tag, tag_idx = "", 0
+
+    for part in re.split(r"(</?(?:b|code)>)", markup):
+        if part == "<b>":
+            tag = "BOLD"
+            tag_idx = len(text)
+        elif part == "</b>":
+            assert tag == "BOLD"
+            len_tagged = len(text) - tag_idx
+            styles.append(f"{tag_idx}:{len_tagged}:{tag}")
+        elif part == "<code>":
+            tag = "MONOSPACE"
+            tag_idx = len(text)
+        elif part == "</code>":
+            assert tag == "MONOSPACE"
+            len_tagged = len(text) - tag_idx
+            styles.append(f"{tag_idx}:{len_tagged}:{tag}")
+        else:
+            text += unescape(part)
+
+    return text, styles
diff --git a/hc/lib/tests/test_html.py b/hc/lib/tests/test_html.py
index 96bb4992..61c745d2 100644
--- a/hc/lib/tests/test_html.py
+++ b/hc/lib/tests/test_html.py
@@ -2,10 +2,10 @@ from __future__ import annotations
 
 from unittest import TestCase
 
-from hc.lib.html import html2text
+from hc.lib.html import extract_signal_styles, html2text
 
 
-class HtmlTestCase(TestCase):
+class Html2TextTestCase(TestCase):
     def test_it_works(self):
         sample = """
             <style>css goes here</style>
@@ -19,3 +19,24 @@ class HtmlTestCase(TestCase):
     def test_it_does_not_inject_whitespace(self):
         sample = """<b>S</b>UCCESS"""
         self.assertEqual(html2text(sample), "SUCCESS")
+
+
+class ExtractSignalTestCase(TestCase):
+    def test_b_works(self):
+        text, styles = extract_signal_styles("<b>foo</b> bar")
+        self.assertEqual(text, "foo bar")
+        self.assertEqual(styles, ["0:3:BOLD"])
+
+    def test_code_works(self):
+        text, styles = extract_signal_styles("foo <code>bar</code>")
+        self.assertEqual(text, "foo bar")
+        self.assertEqual(styles, ["4:3:MONOSPACE"])
+
+    def test_it_rejects_mismatched_tags(self):
+        with self.assertRaises(AssertionError):
+            extract_signal_styles("<b>foo</code>")
+
+    def test_it_unescapes_html(self):
+        text, styles = extract_signal_styles("<b>5 &lt; 10</b>")
+        self.assertEqual(text, "5 < 10")
+        self.assertEqual(styles, ["0:6:BOLD"])
diff --git a/templates/emails/signal-rate-limited-body-html.html b/templates/emails/signal-rate-limited-body-html.html
index 8659c0f7..62cae1ca 100644
--- a/templates/emails/signal-rate-limited-body-html.html
+++ b/templates/emails/signal-rate-limited-body-html.html
@@ -10,7 +10,7 @@ instead:
 </p>
 
 <div style="background: #F2F4F6; padding: 20px;">
-    {{ message|linebreaksbr }}
+    {{ message|safe|linebreaksbr }}
 </div>
 <br />
 {% endblock %}
diff --git a/templates/emails/signal-rate-limited-body-text.html b/templates/emails/signal-rate-limited-body-text.html
index b854ac8c..ecc25efb 100644
--- a/templates/emails/signal-rate-limited-body-text.html
+++ b/templates/emails/signal-rate-limited-body-text.html
@@ -5,7 +5,7 @@ hitting a rate-limit on the Signal network, so we are sending it via email
 instead:
 
 ***
-{{ message }}
+{{ plaintext|safe }}
 ***
 
 Regards,
diff --git a/templates/emails/signal-rate-limited-subject.html b/templates/emails/signal-rate-limited-subject.html
index dcf1889e..30a42dc3 100644
--- a/templates/emails/signal-rate-limited-subject.html
+++ b/templates/emails/signal-rate-limited-subject.html
@@ -1 +1 @@
-Signal notification failed: {{ subject }}
\ No newline at end of file
+Signal notification failed: {{ subject|safe }}
\ No newline at end of file
diff --git a/templates/integrations/signal_message.html b/templates/integrations/signal_message.html
index b1115ed1..d1d6437e 100644
--- a/templates/integrations/signal_message.html
+++ b/templates/integrations/signal_message.html
@@ -1,43 +1,43 @@
 {% load hc_extras humanize linemode %}{% linemode %}
 {% if check.status == "down" %}
-    {% line %}The check “{{ check.name_then_code|safe }}” is DOWN.{% endline %}
+    {% line %}The check <b>{{ check.name_then_code }}</b> is <b>DOWN</b>.{% endline %}
 {% else %}
-    {% line %}The check “{{ check.name_then_code|safe }}” is now UP.{% endline %}
+    {% line %}The check <b>{{ check.name_then_code }}</b> is now <b>UP</b>.{% endline %}
 {% endif %}
 
 {% line %}{% endline %}
 
 {% if check.project.name %}
-{% line %}Project: {{ check.project.name|safe }}{% endline %}
+{% line %}<b>Project:</b> {{ check.project.name }}{% endline %}
 {% endif %}
 
 {% if check.tags_list %}
-{% line %}Tags: {{ check.tags_list|safeseq|join:", " }}{% endline %}
+{% line %}<b>Tags:</b> {{ check.tags_list|join:", " }}{% endline %}
 {% endif %}
 
 {% if check.kind == "simple" %}
-{% line %}Period: {{ check.timeout|hc_duration }}{% endline %}
+{% line %}<b>Period:</b> {{ check.timeout|hc_duration }}{% endline %}
 {% endif %}
 
 {% if check.kind == "cron" %}
-{% line %}Schedule: {{ check.schedule }}{% endline %}
-{% line %}Time Zone: {{ check.tz }}{% endline %}
+{% line %}<b>Schedule:</b> <code>{{ check.schedule }}</code>{% endline %}
+{% line %}<b>Time Zone:</b> {{ check.tz }}{% endline %}
 {% endif %}
 
-{% line %}Total Pings: {{ check.n_pings }}{% endline %}
+{% line %}<b>Total Pings:</b> {{ check.n_pings }}{% endline %}
 
 {% if ping is None %}
-{% line %}Last Ping: Never{% endline %}
+{% line %}<b>Last Ping:</b> Never{% endline %}
 {% elif ping.kind == "ign" %}
-{% line %}Last Ping: Ignored, {{ ping.created|naturaltime }}{% endline %}
+{% line %}<b>Last Ping:</b> Ignored, {{ ping.created|naturaltime }}{% endline %}
 {% elif ping.kind == "fail" or ping.exitstatus > 0 %}
-{% line %}Last Ping: Failure, {{ ping.created|naturaltime }}{% endline %}
+{% line %}<b>Last Ping:</b> Failure, {{ ping.created|naturaltime }}{% endline %}
 {% elif ping.kind == "start" %}
-{% line %}Last Ping: Start, {{ ping.created|naturaltime }}{% endline %}
+{% line %}<b>Last Ping:</b> Start, {{ ping.created|naturaltime }}{% endline %}
 {% elif ping.kind == "log" %}
-{% line %}Last Ping: Log, {{ ping.created|naturaltime }}{% endline %}
+{% line %}<b>Last Ping:</b> Log, {{ ping.created|naturaltime }}{% endline %}
 {% else %}
-{% line %}Last Ping: Success, {{ ping.created|naturaltime }}{% endline %}
+{% line %}<b>Last Ping:</b> Success, {{ ping.created|naturaltime }}{% endline %}
 {% endif %}
 
 {% if down_checks is not None %}
@@ -48,7 +48,7 @@
         {% else %}
             {% line %}The following checks are {% if check.status == "down" %}also{% else %}still{% endif %} down:{% endline %}
             {% for c in down_checks %}
-                {% line %}- “{{ c.name_then_code|safe }}” (last ping: {{ c.last_ping|naturaltime }}){% endline %}
+                {% line %}• <b>{{ c.name_then_code|safe }}</b> (last ping: {{ c.last_ping|naturaltime }}){% endline %}
             {% endfor %}
         {% endif %}
     {% else %}