Skip to content

fix: strip markdown syntax from desktop notification text - #2144

Open
anthhub wants to merge 4 commits into
manaflow-ai:mainfrom
anthhub:fix/strip-markdown-notifications
Open

anthhub wants to merge 4 commits into
manaflow-ai:mainfrom
anthhub:fix/strip-markdown-notifications

Conversation

@anthhub

@anthhub anthhub commented Mar 25, 2026 •

Copy link
Copy Markdown

Summary

  • Fixes Notifications show raw markdown syntax instead of plain text #2044
  • Native macOS notifications triggered by AI agent output (e.g. Claude Code) were displaying raw markdown syntax — **bold**, ## headings, `code`, - list items — instead of clean plain text.
  • Adds a stripMarkdown(_ text: String) -> String private helper on TerminalNotificationStore that removes common markdown formatting before delivering text to UNUserNotificationCenter and the custom notification shell command.

What's stripped

Pattern Before After
Headings ## Task complete Task complete
Bold **important** important
Italic *note* note
Bold+italic ***critical*** critical
Inline code `git status` git status
Links [see docs](https://example.com) see docs
Blockquotes > some quote some quote
Unordered lists - item item
Ordered lists 1. item item
Horizontal rules --- (removed)

Design decisions

  • The raw markdown is preserved in TerminalNotification.body / .subtitle so the in-app notifications panel (which may render markdown in the future) keeps the original text.
  • Only the text sent to UNUserNotificationCenter (content.subtitle, content.body) and to the custom shell command (CMUX_NOTIFICATION_SUBTITLE, CMUX_NOTIFICATION_BODY) is stripped.
  • No third-party dependency. Simple regex replacements are sufficient — a full markdown parser would be overkill for notification text.
  • Title is intentionally not stripped here; notification titles in cmux are typically short, generated tab names or the app name, not markdown prose.

Test plan

  • Trigger a notification from a Claude Code session that outputs markdown (e.g. a response with **bold** or ## Heading) and confirm the desktop banner shows plain text
  • Confirm that the in-app Notifications panel still shows the original text (markdown not stripped from the stored model)
  • Confirm test notification from Settings still works correctly
  • Confirm custom notification command still receives CMUX_NOTIFICATION_BODY as plain text
  • Review unit tests in TerminalNotificationStoreTests pass without regressions

🤖 Generated with Claude Code


Summary by cubic

Fixes #2044. Desktop notifications now strip Markdown from the subtitle and body so macOS shows clean plain text instead of raw formatting.

  • Bug Fixes

    • Added stripMarkdown(_:) in TerminalNotificationStore to remove headings, emphasis, inline code, links, blockquotes, list markers, and horizontal rules; collapses extra blank lines and adds word-boundary guards for italics and underscore-based __bold__/___bold+italic___ to avoid mangling snake_case identifiers.
    • Applied only to payloads sent to UNUserNotificationCenter and the custom notification command; stored notification text stays unchanged. Titles are not modified.
  • Refactors

    • Removed a redundant anchored heading regex in stripMarkdown(_:) to simplify the implementation without changing behavior.

Written for commit f40ff9a. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes
    • Desktop notifications now strip Markdown-like formatting (headings, emphasis, inline code, links, blockquote/list prefixes, horizontal rules), collapse extra blank lines, and trim whitespace so alerts display as plain, readable text.
    • Custom notification sound commands now receive the cleaned plain-text subtitle and body (Markdown removed) when executed.

Fixes manaflow-ai#2044

Native macOS notifications display raw markdown syntax (e.g. **bold**,
## headings, `code`) when triggered by AI agent output. This adds a
stripMarkdown helper that removes common markdown formatting before the
notification body and subtitle are sent to UNUserNotificationCenter and
the custom notification command, so users see clean plain text.

Patterns stripped: headings, bold, italic, inline code, links, blockquotes,
list markers (unordered and ordered), and horizontal rules.
@vercel

vercel Bot commented Mar 25, 2026

Copy link
Copy Markdown

@anthhub is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Mar 25, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ad6e6470-5c56-40ea-96f5-2ae13fcfbe2d

📥 Commits

Reviewing files that changed from the base of the PR and between 238b208 and f40ff9a.

📒 Files selected for processing (1)
  • Sources/TerminalNotificationStore.swift

📝 Walkthrough

Walkthrough

Added a private stripMarkdown(_:) helper that removes common Markdown constructs and applied it to notification subtitle and body when composing UNMutableNotificationContent and when passing those values to NotificationSoundSettings.runCustomCommand.

Changes

Cohort / File(s) Summary
Markdown Stripping for Notifications
Sources/TerminalNotificationStore.swift
Added stripMarkdown(_:) to remove headings, bold/italic/bold-italic, inline code backticks, link markup, blockquote/list markers, and horizontal rules. Replaced raw notification.subtitle/notification.body with stripped text when creating UNMutableNotificationContent and when supplying env vars to NotificationSoundSettings.runCustomCommand.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I hopped through hashes, stars, and ticks,

nibbling markers into tiny bits.
Clean words now bounce where once was markdown,
soft notifications, no more frown.
Carrots, calm, and plain-text grins.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title clearly summarizes the main change: stripping markdown syntax from desktop notification text.
Description check ✅ Passed The description includes all required sections (Summary, Testing, Checklist) with good detail on what changed, why, design decisions, test plan, and references to the linked issue.
Linked Issues check ✅ Passed The PR fully addresses issue #2044 by implementing stripMarkdown() to remove markdown syntax from notification text sent to UNUserNotificationCenter and custom commands while preserving original text in the app model.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the stated objective: adding markdown stripping to TerminalNotificationStore for notifications, with no unrelated modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 1 file

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
Sources/TerminalNotificationStore.swift (1)

1098-1109: Redundant heading regex - first pattern is superseded by the multiline version.

The first regex (lines 1099-1103) with .anchored only matches headings at string start. The second regex (lines 1105-1109) with (?m)^ matches headings at the start of any line, including the start of the string. The first call is therefore redundant.

♻️ Proposed simplification
-        // Headings: ## Heading → Heading
-        result = result.replacingOccurrences(
-            of: #"^#{1,6}\s+"#,
-            with: "",
-            options: [.regularExpression, .anchored]
-        )
-        // Also handle headings mid-line (after a newline)
+        // Headings: ## Heading → Heading
         result = result.replacingOccurrences(
             of: #"(?m)^#{1,6}\s+"#,
             with: "",
             options: .regularExpression
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/TerminalNotificationStore.swift` around lines 1098 - 1109, The first
replacingOccurrences call that uses of: #"^#{1,6}\s+"# with options
[.regularExpression, .anchored] is redundant because the subsequent call using
of: #"(?m)^#{1,6}\s+"# already matches headings at the start of the string and
at the start of any line; remove the initial call (the block that targets ^ with
.anchored) so only the multiline regex replacement on variable `result` remains,
keeping the behavior for both string-start and mid-line headings intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@Sources/TerminalNotificationStore.swift`:
- Around line 1098-1109: The first replacingOccurrences call that uses of:
#"^#{1,6}\s+"# with options [.regularExpression, .anchored] is redundant because
the subsequent call using of: #"(?m)^#{1,6}\s+"# already matches headings at the
start of the string and at the start of any line; remove the initial call (the
block that targets ^ with .anchored) so only the multiline regex replacement on
variable `result` remains, keeping the behavior for both string-start and
mid-line headings intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b35efd8c-e2a9-4957-8f0f-7c3e14b2c7c9

📥 Commits

Reviewing files that changed from the base of the PR and between 99ca3c9 and 2151fb9.

📒 Files selected for processing (1)
  • Sources/TerminalNotificationStore.swift

@greptile-apps

greptile-apps Bot commented Mar 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a stripMarkdown(_ text: String) -> String private helper to TerminalNotificationStore and applies it to the subtitle and body fields sent to UNUserNotificationCenter and the custom notification shell command, fixing the issue where raw markdown syntax was visible in macOS desktop banners. The approach (ordered regex replacements, preserving the original model) is sound, but one regex has a correctness bug that would corrupt technical content.

Key changes:

  • New stripMarkdown helper strips headings, bold, italic, inline code, links, blockquotes, list markers, and horizontal rules via sequential NSRegularExpression replacements.
  • Applied to both scheduleUserNotification and playSuppressedNotificationFeedback call sites, so all notification delivery paths are covered.
  • The original TerminalNotification.body / .subtitle are intentionally left unmodified, preserving raw text for the in-app panel.

Issues found:

  • P1 – _italic_ regex mangles snake_case: _(.+?)_ lacks word-boundary guards and will corrupt identifiers like CMUX_NOTIFICATION_BODY → CMUXBODY or brew_upgrade_formula → brewupgrade_formula. Claude Code output frequently contains such identifiers in notification summaries.
  • P2 – Redundant anchored heading pass: The first replacingOccurrences with .anchored at line 1100 is dead code since the following (?m)^#{1,6}\s+ pass already covers the start of the string.
  • No unit tests added: The PR description references TerminalNotificationStoreTests, but no such test file exists in the repo. Given the non-trivial regex logic and the explicit regression test policy in CLAUDE.md, stripMarkdown is a good candidate for a small unit test suite.

Confidence Score: 3/5

  • Merging as-is will fix the primary markdown-in-notifications pain point, but the _italic_ word-boundary bug means technical identifiers in notification text (file paths, env vars, shell symbols) will be silently mangled.
  • The intent and architecture are correct, and both call sites are properly updated. However, the _(.+?)_ regex is a concrete data-corruption bug — not a hypothetical — because Claude Code output routinely includes snake_case identifiers. Fixing it requires a one-line change (add a (?<!\w) / (?!\w) word-boundary guard). The P1 warrants a targeted fix before merge.
  • Sources/TerminalNotificationStore.swift — specifically the italic _ regex at lines 1141-1145

Important Files Changed

Filename Overview
Sources/TerminalNotificationStore.swift Adds stripMarkdown(_:) helper and applies it to content.subtitle, content.body, and the custom shell command fields. The overall approach is correct, but the _(.+?)_ italic regex does not respect word boundaries and will mangle snake_case identifiers present in AI-generated notification text.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[TerminalNotification arrives] --> B{Suppressed?}
    B -- No --> C[scheduleUserNotification]
    B -- Yes --> D[playSuppressedNotificationFeedback]

    C --> E[resolvedNotificationTitle]
    C --> F["stripMarkdown(subtitle)"]
    C --> G["stripMarkdown(body)"]
    F & G --> H[UNMutableNotificationContent]
    H --> I[UNUserNotificationCenter.add]
    I --> J["runCustomCommand(title, stripped subtitle, stripped body)"]

    D --> K["stripMarkdown(subtitle)"]
    D --> L["stripMarkdown(body)"]
    K & L --> M["runCustomCommand(title, stripped subtitle, stripped body)"]
    D --> N[playSelectedSound]

    subgraph stripMarkdown
        direction TB
        S1[Strip headings] --> S2[Strip bold+italic ***]
        S2 --> S3[Strip bold **]
        S3 --> S4["Strip italic * / _  ⚠️ no word-boundary guard"]
        S4 --> S5[Strip inline code]
        S5 --> S6[Strip links]
        S6 --> S7[Strip blockquotes]
        S7 --> S8[Strip list markers]
        S8 --> S9[Strip horizontal rules]
        S9 --> S10[Collapse blank lines]
        S10 --> S11[Trim whitespace]
    end
Loading

Reviews (1): Last reviewed commit: "fix: strip markdown syntax from desktop ..." | Re-trigger Greptile

Comment on lines +1141 to +1145
result = result.replacingOccurrences(
of: #"_(.+?)_"#,
with: "$1",
options: .regularExpression
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 _italic_ regex mangles snake_case identifiers

The pattern _(.+?)_ does not check word boundaries, so it matches the first pair of underscores in any technical identifier and concatenates the surrounding fragments. For example:

  • brew_upgrade_formula → _upgrade_ is matched → brewupgrade_formula (the leading word loses its separator)
  • CMUX_NOTIFICATION_BODY → _NOTIFICATION_ is matched → CMUXBODY
  • Completed task_1 and task_2 → _1 and task_ is matched → Completed task1 and task2 (data loss between the two items)

This would mangle any notification text that contains technical content with underscores — shell commands, file names, env var names, etc.

Standard Markdown parsers (including CommonMark) do not treat _ as emphasis when it appears inside a word (i.e. flanked by alphanumeric characters on both sides). A word-boundary guard on the italic patterns avoids the false positive:

Suggested change
result = result.replacingOccurrences(
of: #"_(.+?)_"#,
with: "$1",
options: .regularExpression
)
result = result.replacingOccurrences(
of: #"(?<!\w)_(.+?)_(?!\w)"#,
with: "$1",
options: .regularExpression
)

The same word-boundary issue applies to the bold _{2} pattern at lines 1129-1133, though in practice double underscores in non-markdown contexts are far less common.

Comment thread Sources/TerminalNotificationStore.swift Outdated
Comment on lines +1098 to +1109
// Headings: ## Heading → Heading
result = result.replacingOccurrences(
of: #"^#{1,6}\s+"#,
with: "",
options: [.regularExpression, .anchored]
)
// Also handle headings mid-line (after a newline)
result = result.replacingOccurrences(
of: #"(?m)^#{1,6}\s+"#,
with: "",
options: .regularExpression
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Redundant anchored heading pass

The first replacingOccurrences call (lines 1099-1103) uses [.regularExpression, .anchored], which anchors the match to the very start of the string. However, the immediately following call (lines 1105-1109) uses (?m)^#{1,6}\s+ with the multiline flag, which makes ^ match at the start of every line — including the very first character of the string. The first call is therefore entirely redundant: it can never match anything that the second call won't already remove.

Suggested change
// Headings: ## Heading → Heading
result = result.replacingOccurrences(
of: #"^#{1,6}\s+"#,
with: "",
options: [.regularExpression, .anchored]
)
// Also handle headings mid-line (after a newline)
result = result.replacingOccurrences(
of: #"(?m)^#{1,6}\s+"#,
with: "",
options: .regularExpression
)
// Headings: ## Heading → Heading (all lines)
result = result.replacingOccurrences(
of: #"(?m)^#{1,6}\s+"#,
with: "",
options: .regularExpression
)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/TerminalNotificationStore.swift`:
- Around line 1129-1139: The italic-stripping regexes are too broad and mangle
plain text; tighten the two replacingOccurrences calls that operate on result by
adding word-boundary lookarounds so they only match standalone markdown italics
(e.g., use patterns with (?<!\w) and (?!\w) around the delimiters for both the
asterisk and underscore variants), leaving the rest of the replacement logic
intact so result still strips legitimate markdown without corrupting arithmetic
or underscored identifiers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 312b8c01-17dd-4af2-9baf-6590f87727d2

📥 Commits

Reviewing files that changed from the base of the PR and between 2151fb9 and 282c47f.

📒 Files selected for processing (1)
  • Sources/TerminalNotificationStore.swift

Comment thread Sources/TerminalNotificationStore.swift
anthhub and others added 2 commits March 27, 2026 11:46
Bare `_(.+?)_` and `\*(.+?)\*` patterns corrupt snake_case identifiers
like CMUX_NOTIFICATION_BODY by treating embedded underscores/asterisks
as italic delimiters. Wrap both patterns with `(?<!\w)..(?!\w)` so they
only match when the delimiter is not surrounded by word characters.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nderscore regex

Apply the same (?<!\w)...(?!\w) guards to the _{2} and _{3} variants that
were already added to the single-underscore italic regex, preventing false
matches inside snake_case identifiers like my_variable_name or ENV_VAR_NAME.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@anthhub

anthhub commented Mar 31, 2026

Copy link
Copy Markdown
Author

Hi @austinywang @lawrencecchen 👋 Quick ping — this strips markdown syntax from desktop notification text so users see clean readable text instead of raw **bold** markers. Small change, should be a quick review. Thanks!

@anthhub

anthhub commented Apr 27, 2026

Copy link
Copy Markdown
Author

Quick triage request on this PR.

This still looks like a useful quality-of-life bugfix to me, but I know there was some review feedback around the markdown-stripping behavior. If maintainers still want this landed, I can do a final pass on the remaining comments and refresh the branch.

If not, I am happy to close it and avoid leaving stale review surface around.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: notifications Notifications, banners, badges, the bell S3: minor Wrong behavior with a workaround

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Notifications show raw markdown syntax instead of plain text

2 participants