Skip to content

Harden query replies, link display text, and terminal file links - #1534

Merged
nedtwigg merged 8 commits into
mainfrom
terminal-output-hardening
Nov 3, 2026
Merged

nedtwigg merged 8 commits into
mainfrom
terminal-output-hardening

Conversation

@nedtwigg

@nedtwigg nedtwigg commented Nov 3, 2026

Copy link
Copy Markdown
Member

Hardens what terminal output can do through query replies and links, and what invisible characters can hide.

  • OSC 99 reply id. A capability reply reaches the PTY's input, so it now echoes an id only when it matches ^[A-Za-z0-9_+.-]{1,64}$. Any other id is dropped and the query is still answered. Every other reply, from Dormouse, xterm.js, or ImageAddon, carries only a parsed color, a number, or a constant. S124 now states this.
  • Deceptive links. classifyDisplayMatch now compares the host the display text resolves to. It applies NFKC, reads separators as spaces, drops invisible characters, strips wrapping brackets and punctuation, and ignores a root dot or a port. Non-ASCII names go through the URL parser as punycode. Text that names no host (12.5%, v1.2.3) and a file: link whose text names its own path (README.md:42) stay plain.
  • Terminal file: links. A click on a link whose text names its target now waits on the same dialog as a document link when the resolved target would run its own script. The check runs on the resolved target, so a symlink can't answer for its name and a user's inert handler skips the question. A declined question opens nothing further, and a double-click keeps one question. The click reaches the handler through a host-only dispatch option, not request params.
  • Invisible characters. FORMAT_CHARACTERS now covers every Unicode format and default-ignorable code point, and every separator but U+0020. boundedPushText drops the same invisible characters, which also covers pairing and passkey labels.
  • Docs. security.md now states that an upstream Tool grant reaches a dormouse.yml nested in a checkout, and that a confirmed terminal link can send a local page's contents off the machine. The dor-tool rationale no longer assumes output names its own host.

Behavior to review:

  • A Tool's run can no longer spell an emoji that needs U+FE0F (U+200D was already refused); a script it calls still can. Paths and arguments are not refused, but consent dialogs and dor tool now show these characters, and spaces like U+202F in macOS screenshot names, as \u escapes.
  • README.md:42 linking to a non-file: target, such as an https: permalink or vscode://file/..., is now deceptive, as README.md already was. It has a copy action but no open action.

Testing:

  • New tests went red with their fixed lines removed.
  • A real bash and zsh behind the owner stream showed nothing from an out-of-alphabet id stays staged at the prompt.
  • tsc -b in lib and tsc --noEmit in standalone pass.
  • The last root pnpm test, at load average about 90, had one failure: the 300-iteration group-drop test timed out at 5s. An earlier run at similar load also failed IframePanel and frame-client. All three files pass when run alone with --testTimeout=120000 --retry=3, and this branch does not touch them.

🤖 Generated with Claude Code

nedtwigg and others added 6 commits November 2, 2026 19:36
A reply reaches the PTY's input, where a shell with no reader stages it as a
command line. Echo the id only within kitty's own alphabet and a 64-character
bound; any other id is dropped and the query still answered. Every other reply
carries a parsed color, a number, or a constant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The class shown text and consent dialogs escape, and a Tool definition or a link
target refuses, now covers every Unicode format and default-ignorable code point
and every separator but U+0020, not a hand-picked list of them. Tag characters
take the RegExp u flag, and an escape spells each UTF-16 unit so JSON output still
parses back to the same value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A label respelling another host read as a human phrase: a trailing dot, a port,
wrapping brackets or punctuation, compatibility forms, invisible characters, or
a homoglyph. Compare what the text resolves to, through the URL parser, and do
not read a figure, a version, or a file link's own file name as a host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A local file link whose text names its target previews on a click. That text
and an empty host are whatever the output printed, so a page that can read files
beside it now waits on the dialog a document link's does, asked of the resolved
target. A declined question reopens nothing, and a double-click keeps one
question. The security overview also states that an upstream Tool grant reaches a
nested dormouse.yml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Report a terminal click through the host-only dispatch option rather than the
params a client writes, and name the confirmation helpers for both of their
callers. A file link's naming rule is now one predicate, and a run of punctuation
in a long label scans once. Push text and pairing labels drop the same invisible
characters as the consent dialogs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 9d18f57
Status:🚫  Deploy failed.

View logs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nedtwigg
nedtwigg merged commit eb726ec into main Nov 3, 2026
17 of 18 checks passed
@nedtwigg
nedtwigg deleted the terminal-output-hardening branch November 3, 2026 05:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants