`ESC k <text> ESC \` (screen/tmux title sequence) leaks payload as visible text
@ThomasK33 đang làm issue này rồi.
Từ ngày 26/6/2026.
Đánh giá
Issue này chưa được đánh giá.
Mô tả
Problem
When the WASM parser encounters ESC k <text> ESC \, it logs unimplemented ESC action: ESC k and then renders <text> 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 withUint8Arrayor 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.
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:
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"),
`<!doctype html><meta charset="utf-8"><body><div id="t"></div></body>`,
);
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 <text> ESC \ as the screen/tmux title-setting extension and consume it without rendering the payload. Routing <text> 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.
- Ngôn ngữ chính
- TypeScript
- Star
- 2.9k
- Fork
- 174
- Chỉ số merge pull request
- Không có pull request nào được merge trong 30 ngày
Hướng dẫn đóng góp
Chưa lập chỉ mục được hướng dẫn đóng góp cho kho mã nguồn này
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của coder/ghostty-web
-
attachCustomKeyEventHandler inverts xterm.js's return-value contract (silently swallows all input) Đang mở
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 52/100
coder/ghostty-web#192 ·
-
A single throw inside render() permanently stops the render loop, and a consumer cannot restart it Đang mở
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 65/100
coder/ghostty-web#189 ·
-
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 70/100
coder/ghostty-web#188 ·
-
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 45/100
coder/ghostty-web#187 ·
-
Hyperlink arena exhaustion (StringAllocOutOfMemory) spins the write path instead of degrading Đang mở
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 48/100
coder/ghostty-web#186 · 1 bình luận ·
Tất cả issue của coder/ghostty-web
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 84/100
bcgov/bc-wallet-mobile#4761 · 1 bình luận ·
-
external-issue to-triage
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 88/100
-
area-deployment area-integrations triage:bot-seen
Độ khó 2/5 Nửa ngày Mức phù hợp với người mới 86/100
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
-
refactor
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 84/100