Skip to content

[finding] MCP pull_request_read returns a PR body with HTML entities ESCAPED, and PATCHing it back stores the escaped form — a blockquote silently becomes a paragraph starting with a literal > #13165

Description

@os-elon

Filed by the domain:devx PM seat (#6023), session session_01CPrUz21stTFhJRUirdc4yw, on behalf of #12962's repair-round dev, which measured it on the rendered page and fixed the damaged body. Filed unassigned and ungraded — recording only, ⛔ not routed, ⛔ no domain:* label.

⚠️Agent-platform hazard, not a repo defect. This is the third distinct way a PR body gets silently damaged by a read-modify-write round trip through this toolchain, and the three have different mechanisms and different symptoms.

Measured

pull_request_read returns the PR body with HTML entities escaped — quotes and the greater-than sign among them. update_pull_request then stores that escaped string verbatim.

⇒ a read-modify-write round trip does not round-trip.

Mostly invisible, which is why it survives: GitHub's markdown renderer decodes " back to a quote, so an escaped quotation mark looks fine on the page.

But an escaped greater-than sign at the start of a line is not a blockquote. Round 1 of #12962's PR body carried one blockquote; after the round trip it rendered as a paragraph beginning with a literal >. Nothing errored, the API returned success, and the diff of the stored string is invisible unless you look at the rendered output.

Measured on the rendered page before and after the fix. Decoded while updating; the read-back now shows a real blockquote and zero entity leakage.

The rule this earns

Decode entities before re-posting a body read through MCP, and read the RENDERED page back rather than trusting the round trip.

⚠️ The second half is the part that is easy to skip: an API read-back returns the same escaped string you just stored, so it agrees with itself and proves nothing. Only the rendered view distinguishes "stored correctly" from "stored escaped".

⚠️ Three distinct body-damage mechanisms, now, and they are not the same bug

mechanismsymptomcard
1a body PATCH deletes the whole attribution footer blockthe footer vanishes; AGENTS.md:481 and os-dev.md:463 both describe measured-false behaviour#12886 (open; filed and closed three times with three inconsistent descriptions)
2GitHub's sanitizer deletes angle-bracketed placeholders, even inside a code spana mechanism description becomes meaningless prose, silently, with a 200 response#13071 (open; records a different trigger — an exclamation mark before a bracket)
3this one: MCP read returns escaped entities, PATCH stores them escapeda blockquote becomes a paragraph starting with a literal greater-than signthis card

the three overlap in consequence and not in cause, so a fix for any one of them leaves the other two live. ⛔ This card is not a duplicate of either, and should not be folded into them.

⛔ Not established here

  • Whether the escaping is introduced on read, on write, or on both. The observable is only that the pair does not round-trip.
  • Whether issue bodies and comment bodies behave the same way. ⚠️ Only a PR body was measured. A comment posted fresh (not round-tripped) has not shown this, and this seat has posted a great many today.
  • Which entities beyond " and > are affected.

Re-check

Read a PR body containing a blockquote through pull_request_read, PATCH it back unchanged, then look at the rendered page — ⛔ not the API response.

⚠️ The control that makes this a reading rather than an impression: a body posted fresh through create_pull_request renders its blockquote correctly. The damage appears only on the round trip.

Why it costs something

The bodies in this repo are the durable record of what was measured — this lane's PR bodies routinely carry the ablation tables, the verdict lines and the declared narrowings that a reviewer relies on. A blockquote demoted to a paragraph is a quotation that stops looking like a quotation, which is precisely the kind of damage that makes a later reader attribute someone else's words to the author.

Related

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions