`ESC k <text> ESC \` (screen/tmux title sequence) leaks payload as visible text
- Dominant language
- TypeScript
- Stars
- 2.8k
- Forks
- 172
- PR merge metrics
- No merged PRs in 30d
Description
## Problem
When the WASM parser encounters `ESC k ESC \`, it logs `unimplemented ESC action: ESC k` and then renders `` onto the terminal grid, silently consuming the trailing `ESC \`. This is the GNU screen / tmux title-setting extension — the payload should never appear on the display.
OSC `0;` / `2;` title-setting in the same build works correctly (payload is consumed, not painted), so this is specific to the `ESC k` code path.
## Expected result
```
0: ""
1: "demo.txt"
```
Neither title string (`/tmp`, `ls`) should appear on the grid.
## Actual result
```
0: "/tmp"
1: "lsdemo.txt"
```
Both title strings leak. The second one is jammed into the first line of visible output, which on a real shell manifests as the running command's name concatenated with whatever it prints.
Console (per `term.write()` call):
```
[ghostty-vt] warning(stream): unimplemented ESC action: ESC k
[ghostty-vt] warning(stream): unimplemented ESC action: ESC k
```
Also verified:
- Same result whether `term.write()` is called with `Uint8Array` or with a plain string.
- Same build correctly consumes `ESC ] 0 ; /tmp BEL` (OSC 0) without leaking.
## Reproduction
Standalone. `ghostty-web@0.4.0`, headless Chromium 146, macOS 15 arm64, Node 25.
```bash
mkdir ghostty-repro && cd ghostty-repro
npm init -y >/dev/null
npm install ghostty-web puppeteer
# save the file below as repro.mjs, then:
node repro.mjs
```
`repro.mjs`:
```js
import http from "node:http";
import fs from "node:fs/promises";
import path from "node:path";
import puppeteer from "puppeteer";
const ROOT = path.dirname(new URL(import.meta.url).pathname);
const PORT = 5765;
// WASM needs to load over http(s), not file://, so serve the installed
// ghostty-web package from a tiny local static server.
const server = http.createServer(async (req, res) => {
const url = req.url === "/" ? "/index.html" : req.url;
const local = url.startsWith("/ghostty-web/")
? path.join(ROOT, "node_modules/ghostty-web", url.slice("/ghostty-web/".length))
: path.join(ROOT, url);
try {
const data = await fs.readFile(local);
const ext = path.extname(local);
res.writeHead(200, {
"Content-Type":
ext === ".js" || ext === ".mjs" ? "text/javascript" :
ext === ".wasm" ? "application/wasm" :
ext === ".html" ? "text/html" : "application/octet-stream",
});
res.end(data);
} catch {
res.writeHead(404).end();
}
});
await new Promise(r => server.listen(PORT, r));
await fs.writeFile(
path.join(ROOT, "index.html"),
`
);
const browser = await puppeteer.launch({ headless: "new" });
const page = await browser.newPage();
page.on("console", m => {
const t = m.text();
if (t.includes("unimplemented") || t.includes("warning")) console.log(" [page]", t);
});
await page.goto(`http://localhost:${PORT}/`);
const lines = await page.evaluate(async () => {
const mod = await import("/ghostty-web/dist/ghostty-web.js");
const ghostty = await mod.Ghostty.load();
const term = new mod.Terminal({ cols: 80, rows: 5, ghostty });
term.open(document.getElementById("t"));
// ESC k /tmp ESC \ (screen/tmux title-set #1)
// \r\n
// ESC k ls ESC \ (screen/tmux title-set #2)
// demo.txt\r\n (visible body)
term.write(new Uint8Array([
0x1b, 0x6b, 0x2f, 0x74, 0x6d, 0x70, 0x1b, 0x5c,
0x0d, 0x0a,
0x1b, 0x6b, 0x6c, 0x73, 0x1b, 0x5c,
0x64, 0x65, 0x6d, 0x6f, 0x2e, 0x74, 0x78, 0x74, 0x0d, 0x0a,
]));
await new Promise(r => setTimeout(r, 150));
const out = [];
const active = term.buffer.active;
for (let y = 0; y < term.rows; y++) {
const line = active.getLine(y);
if (line) out.push(line.translateToString(true));
}
return out;
});
console.log("\nRendered buffer:");
lines.forEach((l, i) => console.log(` ${i}: ${JSON.stringify(l)}`));
await browser.close();
server.close();
```
Running it prints:
```
[page] [ghostty-vt] warning(stream): unimplemented ESC action: ESC k
[page] [ghostty-vt] warning(stream): unimplemented ESC action: ESC k
Rendered buffer:
0: "/tmp"
1: "lsdemo.txt"
2: ""
3: ""
4: ""
```
## Suggested fix
Recognize `ESC k ESC \` as the screen/tmux title-setting extension and consume it without rendering the payload. Routing `` to a title-change listener (as OSC 0/2 already do) would be a bonus but isn't required — silently discarding is enough to fix the visible bug.
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.