Skip to content

fix(notes): escape few-shot example interpolation to stop prompt self-poisoning - #572

Merged
goosewobbler merged 1 commit into
mainfrom
fix/escape-example-interpolation
Jul 19, 2026
Merged

goosewobbler merged 1 commit into
mainfrom
fix/escape-example-interpolation

Conversation

@goosewobbler

@goosewobbler goosewobbler commented Jul 19, 2026 •

Copy link
Copy Markdown
Owner

Closes #571. Carved out of #542 (the eval-harness epic) as a standalone 0.41.0 fix.

Problem

renderExamplesBlock (packages/notes/src/llm/examples/parser.ts) interpolated example fields — version, category, scope, leadIn, description — into the prompt's <example> block unescaped. The examples are built from past release bodies (parseReleaseBodyToExample → fetchExamples), and release-notes output becomes a future example — a self-poisoning loop: an entry containing </example> or marker/instruction-like text could forge a closing tag or inject instructions into a later prompt.

Same unescaped-interpolation class already fixed for PR bodies (renderPRBlocks uses escAttr/escBody) and for task-prompt entry rendering (#540, #565).

Fix

Escape every interpolated field, reusing the existing helpers in packages/notes/src/llm/tasks/shared.ts:

  • version (attribute) → escAttr
  • category / scope / leadIn / description (body) → escBody

No behavior change for well-formed input — it only neutralizes tag/marker-forging content. Added a renderExamplesBlock spec that feeds hostile fields and asserts they're escaped (and the block's own </example> is the only literal one left).

Verification

pnpm turbo run lint typecheck test --filter=@releasekit/notes — green (403 tests, incl. the new one).

🤖 Generated with Claude Code

Greptile Summary

This PR closes the prompt self-poisoning loop in renderExamplesBlock by HTML-escaping every interpolated field derived from past release bodies before they enter the few-shot <example> block. Without this fix, a crafted release description containing </example> or instruction-like text could forge a closing tag or inject markup into a later prompt.

  • version (XML attribute) is now escaped with escAttr; category, scope, leadIn, and description (body text) are escaped with escBody — reusing the same helpers already applied to <pr> and <entry> blocks.
  • A new renderExamplesBlock spec feeds hostile values in every field and asserts all </>/& characters are encoded, with exactly one literal </example> (the block's own closing tag) remaining.

Confidence Score: 5/5

Safe to merge — the escaping is correct, complete, and consistent with the existing pattern used for PR and entry blocks.

Every interpolated field in renderExamplesBlock is now escaped at render time with the right helper (attribute vs body context), the raw-then-escape flow through the cache is correct, and the new test exercises hostile values in all five fields with both negative and positive assertions.

No files require special attention.

Important Files Changed

Filename Overview
packages/notes/src/llm/examples/parser.ts Adds escaping for all interpolated fields in renderExamplesBlock using the correct helpers (escAttr for version attribute, escBody for body content); fix is complete and consistent with the existing PR/entry escaping pattern.
packages/notes/test/unit/examples.spec.ts Adds a targeted hostile-input test for renderExamplesBlock covering all five interpolated fields; asserts both negative (raw tags absent) and positive (entity-encoded forms present) conditions, plus the single-literal-closing-tag invariant.

Reviews (2): Last reviewed commit: "fix(notes): escape few-shot example inte..." | Re-trigger Greptile

@goosewobbler goosewobbler added this to the 0.41.0 milestone Jul 19, 2026
@github-actions

github-actions Bot commented Jul 19, 2026 •

Copy link
Copy Markdown
Contributor

Standing release PR: #555 · v0.41.0 queued (4 packages) · open 5h 13m · ✅ ready to merge

Release Preview — v0.40.0 → v0.40.1

Note: Labels on this PR are advisory in standing-pr mode. Bumps come from conventional commits in the standing PR; override by editing labels on the standing PR itself. Add release:immediate to bypass the standing PR and release this PR directly.

These changes will be added to the release PR (#555) when merged:

Changelog

monorepo 0.40.0 → 0.40.1

Fixed

  • escape few-shot example interpolation to stop prompt self-poisoning (notes) (#571)

All packages → 0.40.1 (sync versioning)

  • @releasekit/version
  • @releasekit/notes
  • @releasekit/publish
  • @releasekit/release

After merge — predicted release

No version escalation — this PR's changes will be included in the queued release without affecting the projected versions.

Package Standing PR This PR After merge
monorepo 0.41.0 0.40.1 0.41.0

Updated automatically by ReleaseKit

@goosewobbler
goosewobbler force-pushed the fix/escape-example-interpolation branch from 8a24e00 to 8734b0f Compare July 19, 2026 19:13
…-poisoning

renderExamplesBlock interpolated example fields (version, category, scope,
leadIn, description) into the prompt's <example> block unescaped. Examples
are built from past release bodies and release-notes output becomes a
future example — a self-poisoning loop where an entry could forge a
</example> tag or inject instructions into a later prompt.

Escape every interpolated field, reusing the existing escAttr/escBody
helpers (as already done for PR bodies in renderPRBlocks). Same
prompt-injection hardening family as #540/#565.

Closes #571

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011i1KvoyxATU3bz5V5Sf1Xu
@goosewobbler
goosewobbler force-pushed the fix/escape-example-interpolation branch from 8734b0f to 591b9e7 Compare July 19, 2026 19:18
@goosewobbler
goosewobbler merged commit 9ffc66d into main Jul 19, 2026
24 checks passed
@goosewobbler
goosewobbler deleted the fix/escape-example-interpolation branch July 19, 2026 19:23
This was referenced Jul 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LLM notes: escape few-shot example interpolation in renderExamplesBlock (self-poisoning prompt injection)

1 participant