Somewhere in PIM’s output area, a project card renders with a clickable link that opens it — /open <projectId> baked into an onclick handler as a plain JavaScript string. That’s fine as long as projectId is a UUID PIM generated itself. It stops being fine the moment any part of that handler is built from text a user actually typed, because a single stray quote character in that text can close the JavaScript string early and let whatever comes after it run as code instead of sitting there as data.

We found this exact shape three separate times in the same week, working through Plan’s new visualizations. The first instance was pre-existing — a project’s free-text name spliced directly into ProjectViewRenderer’s generated onclick string, sitting there from an earlier session. Fixed by moving the id into an HTML-escaped data-project-id attribute instead, read back at click time rather than embedded in the JavaScript source at all — the standard fix for exactly this shape of bug.

Then it came back. Building Project’s new Gantt-style timeline view a few days later, the exact same pattern got written fresh into a brand-new class, TimelineBar, this time for the click-through that opens a project from its own timeline bar. Same root cause, same fix, just in code that hadn’t existed a week earlier — proof that knowing about a bug once doesn’t automatically stop you from reintroducing its shape somewhere new.

The third time was different. Adding drag-and-drop to the same two boards for kanban, the exact same class of mistake was available again — a free-text project name or plan item title needing to travel from a drag gesture to a drop handler — and this time it got caught before it ever shipped, using HTML5’s own DataTransfer object to carry the value across the drag instead of splicing it into generated JavaScript at all.

The actual fix, both times it mattered, is almost boring: never let a value you don’t fully control become part of the JavaScript source string itself. A UUID or a fixed status name from a known list is safe to splice in directly, because there’s nothing a user could type into it that would contain a quote. Anything a user or the AI actually wrote — a project name, an item title — goes into an HTML attribute or a DataTransfer payload instead, and gets read back as data, never executed as code. The pattern is now something to check for by reflex whenever a new renderer wraps a free-text field into a click or drag handler, rather than something to rediscover the hard way a fourth time.