coder / coder/ghostty-web

`ESC k <text> ESC \` (screen/tmux title sequence) leaks payload as visible text

Open
#153 2 comments 0 reactions 1 assignee Claimed by @ThomasK33 View on GitHub
accepted bug triage:done
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.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.