What brokeUpdated 3 October 2026
Hand-edited captions skipped LinkedIn escaping
In the newsroom engine's sharing queue, a caption edited by hand replaced the rendered post text and skipped the LinkedIn adapter's escaping. A parenthesis in a hand-written caption would have been read as markup. We found it by looking at the approval screens rendered against real articles.
The short version
What went wrongA caption edited by hand in the approval queue replaced the rendered text and skipped LinkedIn's escaping, so a parenthesis could be read as markup. The length check also measured unescaped text.
How we found itLooking at the approval screens rendered against real articles turned up the correctness bug.
The rule nowAny text reaching a channel, generated or typed, goes through that channel's preparation step right before sending. Length limits are checked after it, on the exact outgoing text.
This post documents something Claude got wrong. The claim rests on a commit or a file, not on memory. All of Claude's mistakes
The newsroom engine can share kokkekunst.no stories to Bluesky, Discord and LinkedIn, but only after approval. A new “Deling” (sharing) tab in admin shows each story as each channel would display it: the text, the link card and the share image. From there you approve it, edit the text first, or skip it. Nothing goes out without approval unless autopilot is switched on for a target.
The “edit the text first” option had a bug. A caption edited in the approval queue went to LinkedIn without LinkedIn’s escaping.
What went wrong
LinkedIn reads post text in its own “little” format. In that format, characters like parentheses, square and curly brackets, @, #, *, _ and ~ mean something. Literal text needs a backslash in front of each one. The fix commit points to render(), the function that turns an article into a post for a given channel: an edited caption never passed through render(), so LinkedIn never escaped it.
A hand-edited caption did not pass through it. The processor rendered the post as usual, and then, if an editor had changed the caption, replaced the rendered text with the edited version as it stood. The rendered text was thrown away, and the unescaped human text went on the wire.
The commit that fixed it puts it this way:
a caption edited in the approval queue never passes through render(), so LinkedIn never escaped it and a parenthesis in a hand-written caption would have been read as markup.
The length check had the same gap. It measured the raw edited caption against LinkedIn’s character limit. Escaping adds a backslash for each special character, so a caption that passed the check could get longer once it was made ready for the wire. Since the edited caption was never escaped, the check measured the right thing by accident. Once escaping was added, the check had to come after it.
Henning writes no code himself, so the original code path was Claude’s.
How we found it
The commit says that “seeing the screens rendered against real articles turned up a correctness bug.” We were looking at the approval screens filled with real stories.
The same look at the screens turned up several smaller problems that went into the same commit:
- The article title showed three times in every row: as a heading, in the preview text, and on the link card. The heading now only appears as a fallback when a preview cannot be built. The link to the story moved down to the row’s footer.
- On screens wider than 60rem, the preview now puts the text and the link card side by side. The commit says this “roughly halves the height of a row”. That is the commit’s own estimate, not a measurement we took.
- Account action buttons no longer stretch to full width.
- A link styled as a button is no longer underlined.
These are layout fixes. The caption bug is the one that mattered.
The fix
The adapter interface got two optional methods, each doing the opposite of the other:
prepareCaptionturns a human-written caption into what the wire needs. For LinkedIn, that means escaping it.previewTextturns a rendered post back into what a reader will see. For LinkedIn, that means removing the backslashes.
The processor now calls prepareCaption on an edited caption, runs the length check on the result, and only then puts it into the post. Channels that don’t need escaping skip the method, and their captions go through unchanged.
previewText fixes a related problem in the other direction. When no edited caption exists, the fix passes the rendered text through previewText before the approval queue shows it, because for LinkedIn the rendered text is the escaped wire version. Showing those backslashes in the preview would have made our own text look wrong. The queue now shows the readable version of a generated caption. An edited caption is shown as it was typed, and the wire format is added on the way out. previewText is display only and never published.
Two tests came with the fix. One registers a test adapter whose prepareCaption adds a visible marker. It approves a job with a hand-written caption containing parentheses, runs the publish step, and checks that the published text has the marker on it. That shows the processor actually calls the method. The other test checks the LinkedIn adapter directly: a short caption with a word in parentheses comes out with both parentheses escaped. It also checks that the readable preview contains no backslashes and that unescaping an escaped string gives back the original.
Why it was easy to miss
The length check was correct for the input it saw, and that input was not what would actually be sent.
The rule
Escaping belongs on the way out to the service, not only in generation. Any text that reaches a channel, whether generated or typed by a person, passes through the channel’s own preparation step right before it is sent. Length limits are checked after that step, on the exact text that will go out, because that is the text the service counts.
Corrections
- Attribution corrected: an earlier version said the material did not show who wrote the code and did not put the mistake on Claude. Henning writes no code himself; the code was written by Claude.