Summary
analyze.EmitRequest writes the manifest's app, participant, and tasks fields into the emitted analysis request through session.SafeText only. SafeText strips control/format/bidi bytes but deliberately passes the inline-Markdown metacharacters \ ` * _ [ ] ( ) ! < > ~ through unchanged. These three fields are rendered as plain Markdown list items outside any code fence, so an attacker-authored manifest value survives into the request as live Markdown — a tracking/exfil image beacon or an active link — the exact construct report.md already neutralizes for the identical fields via mdInline.
Wrong implicit assumption
emit.go's own comment (lines 82–96) asserts these strings "go through session.SafeText, matching report and review," and reasons only about block-structure forgery (a newline-led ## heading, which SafeText's newline stripping does prevent). It never considers inline Markdown, which needs no newline. The parity claim is false: report.go renders the same fields through mdOrDash → mdInline, which applies SafeText and then backslash-escapes each inline trigger (report.go:324-336). emit.go omits that second step.
Exact trigger (input/state → observed vs correct)
-
An exchanged/downloaded session ships an attacker-authored manifest.json (session.SaveManifest/LoadManifest validate only total size, never field content):
{"session":"2026-08-18_101500","app":"[x](http://attacker.example/beacon.png)","participant":"P1","t0_epoch_ms":1,"tasks":["Explore [y](http://attacker.example/t.png)"]}
-
Operator runs testimony analyze -session DIR -out sessions/DIR/request.md (the -out path cli.go:371 itself gives as the canonical destination).
-
The request's ## Session block contains, verbatim:
- App: [x](http://attacker.example/beacon.png)
- Observed: when
request.md is opened in any Markdown viewer (VS Code preview, GitHub, Obsidian) or handed to a colleague who previews it, the image URL is fetched → tracking/exfil beacon fires; an [label](http://attacker.example) value becomes an active link embedded in the instructions the model is asked to obey.
- Correct: the inline-Markdown metacharacters are backslash-escaped (as
mdInline does for the same fields), rendering as inert literal text. An ordinary app/participant/task name carries none of these bytes and is byte-for-byte unchanged.
Location
internal/analyze/emit.go:98-99 — App and Participant via safeOrNone
internal/analyze/emit.go:109 and :116 — Tasks via bare session.SafeText(t)
internal/analyze/emit.go:153-159 — safeOrNone applies session.SafeText only, no escaping
- Contrast the hardened sibling:
internal/report/report.go:101-113 renders the identical fields via mdOrDash/mdInline (report.go:324-336), which escapes the triggers.
CWE anchor
CWE-116 (Improper Encoding or Escaping of Output); the beacon/active-link effect is markup injection in the same family report.go's mdInline comment names for report.md.
Sibling-sweep result
- App, Participant, Tasks (
emit.go) — all three share the gap (all rendered outside any fence with SafeText only). All affected.
## Timeline block (emit.go:125-136) — NOT a sibling: each entry is one json.Marshal object per physical line inside a ```jsonl fence, where inline Markdown does not render; SafeText there is sufficient.
report.md (report.go) and review terminal output (review.go) — already hardened (mdInline/SafeText-for-terminal). The emitted request is the one remaining Markdown artifact built from untrusted manifest text that does not escape inline Markdown — i.e. the lone unswept sink.
Scope caveat (for the maintainer)
The beacon's image fetch requires the request to reach a Markdown renderer — saving -out request.md and previewing it, or handing the .md file to a colleague. The documented pure stdin | assistant path (an LLM API that does not fetch URLs) does not fire the beacon; the terminal path does not render Markdown. The unescaped-injection reach holds regardless, and the repo already ruled the identical construct in-scope for the report.md sibling.
Validator confirmations (both lenses, adversarial, independent)
- Reachability lens — CONFIRMED: "manifest
app/participant/tasks reach the emitted request through safeOrNone/SafeText only (emit.go:98-99, 109, 153-159), outside any code fence, with the inline-Markdown escaping the sibling report.md path applies via mdInline (report.go:324-337) omitted; the beacon/active-link payload the repo already treats as in-scope for report.md (report.go:312-323) survives byte-for-byte."
- Correctness lens — CONFIRMED: "
SafeText demonstrably preserves the inline-Markdown metacharacters (session.go:375-394, default: return r); App/Participant/Tasks sit in raw Markdown context with no escaping and no fence; report.go renders the identical fields via mdInline which escapes them — a genuine divergence, and emit.go's own 'matching report' parity claim is false at the safety layer."
Fix direction (one line, not prescriptive)
Route the three fields through the same inline-Markdown escaping report.go uses (or a shared helper), so the request neutralizes inline constructs as report.md already does.
Summary
analyze.EmitRequestwrites the manifest'sapp,participant, andtasksfields into the emitted analysis request throughsession.SafeTextonly.SafeTextstrips control/format/bidi bytes but deliberately passes the inline-Markdown metacharacters\ ` * _ [ ] ( ) ! < > ~through unchanged. These three fields are rendered as plain Markdown list items outside any code fence, so an attacker-authored manifest value survives into the request as live Markdown — a tracking/exfil image beacon or an active link — the exact constructreport.mdalready neutralizes for the identical fields viamdInline.Wrong implicit assumption
emit.go's own comment (lines 82–96) asserts these strings "go throughsession.SafeText, matching report and review," and reasons only about block-structure forgery (a newline-led##heading, whichSafeText's newline stripping does prevent). It never considers inline Markdown, which needs no newline. The parity claim is false:report.gorenders the same fields throughmdOrDash → mdInline, which appliesSafeTextand then backslash-escapes each inline trigger (report.go:324-336).emit.goomits that second step.Exact trigger (input/state → observed vs correct)
An exchanged/downloaded session ships an attacker-authored
manifest.json(session.SaveManifest/LoadManifestvalidate only total size, never field content):{"session":"2026-08-18_101500","app":"[x](http://attacker.example/beacon.png)","participant":"P1","t0_epoch_ms":1,"tasks":["Explore [y](http://attacker.example/t.png)"]}Operator runs
testimony analyze -session DIR -out sessions/DIR/request.md(the-outpathcli.go:371itself gives as the canonical destination).The request's
## Sessionblock contains, verbatim:request.mdis opened in any Markdown viewer (VS Code preview, GitHub, Obsidian) or handed to a colleague who previews it, the image URL is fetched → tracking/exfil beacon fires; an[label](http://attacker.example)value becomes an active link embedded in the instructions the model is asked to obey.mdInlinedoes for the same fields), rendering as inert literal text. An ordinary app/participant/task name carries none of these bytes and is byte-for-byte unchanged.Location
internal/analyze/emit.go:98-99— App and Participant viasafeOrNoneinternal/analyze/emit.go:109and:116— Tasks via baresession.SafeText(t)internal/analyze/emit.go:153-159—safeOrNoneappliessession.SafeTextonly, no escapinginternal/report/report.go:101-113renders the identical fields viamdOrDash/mdInline(report.go:324-336), which escapes the triggers.CWE anchor
CWE-116 (Improper Encoding or Escaping of Output); the beacon/active-link effect is markup injection in the same family
report.go'smdInlinecomment names forreport.md.Sibling-sweep result
emit.go) — all three share the gap (all rendered outside any fence withSafeTextonly). All affected.## Timelineblock (emit.go:125-136) — NOT a sibling: each entry is onejson.Marshalobject per physical line inside a```jsonlfence, where inline Markdown does not render;SafeTextthere is sufficient.report.md(report.go) and review terminal output (review.go) — already hardened (mdInline/SafeText-for-terminal). The emitted request is the one remaining Markdown artifact built from untrusted manifest text that does not escape inline Markdown — i.e. the lone unswept sink.Scope caveat (for the maintainer)
The beacon's image fetch requires the request to reach a Markdown renderer — saving
-out request.mdand previewing it, or handing the.mdfile to a colleague. The documented purestdin | assistantpath (an LLM API that does not fetch URLs) does not fire the beacon; the terminal path does not render Markdown. The unescaped-injection reach holds regardless, and the repo already ruled the identical construct in-scope for thereport.mdsibling.Validator confirmations (both lenses, adversarial, independent)
app/participant/tasksreach the emitted request throughsafeOrNone/SafeTextonly (emit.go:98-99, 109, 153-159), outside any code fence, with the inline-Markdown escaping the siblingreport.mdpath applies viamdInline(report.go:324-337) omitted; the beacon/active-link payload the repo already treats as in-scope forreport.md(report.go:312-323) survives byte-for-byte."SafeTextdemonstrably preserves the inline-Markdown metacharacters (session.go:375-394,default: return r); App/Participant/Tasks sit in raw Markdown context with no escaping and no fence;report.gorenders the identical fields viamdInlinewhich escapes them — a genuine divergence, andemit.go's own 'matching report' parity claim is false at the safety layer."Fix direction (one line, not prescriptive)
Route the three fields through the same inline-Markdown escaping
report.gouses (or a shared helper), so the request neutralizes inline constructs asreport.mdalready does.