Invisible characters in pasted commands are made visible, and control sequences in terminal
output are neutralized, so neither input nor output can hide what's really there.
The same file, cat'd in two terminals with the same font. Left,
secure-terminal in Show mode; right, a traditional terminal (GNOME Terminal).
It is a routine "verify this release before you deploy" checklist -- and every reassuring
line hides a still-applicable trick, spread left to right: a Cyrillic look-alike in two
example.com addresses (U+0430), a right-to-left override that shows an .exe as a .pdf, a
zero-width character inside "administrator", a silently hijacked window title, and a silent
OSC-52 clipboard overwrite. The traditional terminal renders all of it as a trustworthy
session; secure-terminal shows the same bytes and flags every one. Real screenshots, captured
under a labwc compositor; title-hijack behaviour varies by terminal (konsole, for instance,
resets it).
Drag to compare, or click a side to open it full size.
What is this? A terminal whose display shows printable ASCII in its default display mode (everything else is
neutralized to a visible, inert placeholder) and which runs no escape-sequence parser on program output
beyond safe colour and four edits clamped to the line being written.
Paste a command you copied from the web, or read text a program printed, and trust that what you see
is what is there. When you need more, an opt-in TUI mode runs full-screen programs, ssh, vim, htop,
through a restricted screen model that still filters every character.
Program output is rendered as printable ASCII plus newline and tab. Bidi overrides, zero-width characters,
homoglyphs, ANSI escapes and control bytes never reach the screen as anything but their neutralized form.
A hostile filename in ls, a
forged [ OK ] in a log, a
Trojan-Source comment:
none of them can reorder what you read or repaint a line you have already scrolled past.
One honest caveat: a program can still redraw the line it is currently writing -- see
what a program can still do to its own line.
By default there is no general ANSI parser to attack: no vertical or absolute cursor movement, no alternate
screens, no OSC hyperlinks, no window-title control, no clipboard-write escape (OSC 52). Parsing
attacker-influenced escapes is the entire vulnerability class; a terminal that will not parse them cannot
fall for them. Two narrow exceptions are honoured on purpose - safe, contrast-guarded colour, and the four
line-local edits a shell's line editor emits, which cannot leave the line being
written. Each OSC feature is a separate, off-by-default toggle you may enable at your own risk, and
turning one on dims a security indicator so the extra surface is never invisible.
One honest caveat, stated narrowly. By default, CLI mode honours the four line-local edits a
shell's line editor emits (cursor forward/back, cursor-to-column, erase-in-line), all clamped to the
line currently being written - so a program can overwrite what it itself just printed on that line,
exactly as a \r progress bar does, but nothing can reach a line you have already scrolled
past. Transcript integrity is per-line: everything above is append-only, and the overwritten text
is gone (capture it with stcat
if you need a byte-exact record). And it is configurable: drop even this line-local editing with
View -> Line editing (line_edits, on by default), and a program can then no longer
alter even its own current line. Full detail on the compatibility page.
You type printable ASCII and the usual control keys (Ctrl+C, Ctrl+A,
Ctrl+R and the rest). They send the control byte exactly as a real terminal does, so a shell in
its normal line mode interrupts on Ctrl+C while a raw-mode program - an editor, or a CLI that shows
"press Ctrl+C again to exit" - reads the byte itself. A signal key is echoed in caret notation
(^C, ^\) so you always see the interrupt you sent, even when the shell would
print nothing. A Terminate button (SIGTERM then SIGKILL) is the guaranteed escape hatch for a
program that ignores everything, so you can never get stuck.
The code that reads hostile bytes is written in a memory-safe language (Python). The traditional terminal
emulators, written in C, have a
long history of memory-corruption bugs
in exactly this parsing path (escape and image-decoder overruns); here
that entire class, buffer overflows and use-after-free driven by attacker-controlled output, cannot happen
by design.
The default font is Hack, chosen for
disambiguation, not looks. Its glyphs are drawn so the characters attackers rely on being
confusable are not: 0 vs O, 1 vs l vs
I, rn vs m all read distinctly, so a look-alike cannot hide
behind an ordinary glyph. It ships no ligatures or contextual alternates - and secure-terminal
disables them anyway - because a ligature merges characters visually (!=
drawn as a single ≠), hiding what the bytes really are in a terminal whose whole promise is
that what you see is what is there. The default font is a hard dependency, enforced at
startup: if Hack is missing the app fails loud rather than let the toolkit silently
substitute some other font that could reintroduce the confusable glyphs or ligatures it exists
to avoid.
Each tab's shell runs in its own Linux control group with a process-count ceiling and a memory
cap. A fork bomb hits its own PID limit while the other tabs keep their share; a program that runs
away on memory is stopped inside its own cgroup, so the runaway is killed instead of the whole
terminal or a sibling tab. The memory cap is sized from what is actually free at the time, so it
trips before the system does. This is resource containment, not a sandbox - the programs you
launch are your own commands and run with your own privileges; it bounds an accidental or crude
runaway, it is not an escape boundary (isolating the terminal from the rest of the system is your
OS's job). Best-effort by design: where the OS does not delegate cgroup v2, a tab simply falls
back to the session's own limits, no worse than any other terminal, never a blocked shell.
Per tab: Box (every non-ASCII character becomes an inert box, coloured by risk class),
Reveal (each as a <U+XXXX> badge, to inspect exactly what is there), Detail
(the same badge with the codepoint's Unicode name inline, the safe default), and Show (render
legitimate unicode so you can read a log, each glyph still tinted by risk class (except inert box-drawing
and block elements, drawn in the program's own colour) - a homoglyph confusable
with ASCII wears a louder colour than honest foreign text, and a non-ASCII space is shown as a distinct
marker and copies as _, never as a space). The figure below is one ls listing
in all four.
Show does not mean run. All four modes remove escape sequences first (there is no
general escape parser in CLI mode), so nothing a program prints - a title change, a clipboard write, an
alternate screen, a jump to another line - is ever interpreted, in any mode, Show included. The mode
only changes how the remaining, already-safe characters are drawn, never whether escapes are honoured; the
only escapes honoured in every mode are safe colour and the four line-local edits described under
the current line.
The same ls listing, all four modes - three of the
filenames are lying (a right-to-left override, a Cyrillic look-alike, a zero-width space) and two are
honest non-ASCII (résumé.pdf, a CJK name).This is also the honest answer to "is it ASCII only?" - it is not, and the
panels show why. Box is ASCII in text but draws a non-ASCII placeholder glyph, and it boxes
honest non-ASCII too. Show deliberately keeps real printable non-ASCII, tinting a
look-alike rather than removing it. Reveal and Detail are pure ASCII, naming what was
hidden. What holds in every mode is narrower and more useful: no invisible, bidi or control character
reaches you unmarked.
Paste anything with unicode or control characters and a review bar opens inside the window,
holding the paste before a byte reaches the shell. It shows the paste four ways - as it looks, each
hidden character named inline as a <U+XXXX> badge, and exactly what each send button
would deliver (stripped to ASCII, or with printable unicode kept) - all risk-coloured by the terminal's own
pipeline. Input is suspended and both send buttons are countdown-gated while it is held, so a stray key
reaches nothing. When to warn is configurable (always / only on unicode-or-control, the default / never);
never turns off the prompt, not the safety - a paste is always sanitised to ASCII, there is no
pass-through mode. A multi-line paste is held too, so a hidden second command cannot run the instant you
paste.
Copy is reviewed the same way, in the other direction - the same bar guards text leaving
via copy, configured separately because copy and paste are opposite trust directions. The two screenshots
below show both bars, and the review bar page walks through every part.
Save a tab's scrollback to a file. The saved file is lossless - each neutralized character is
named inline (<U+0430 CYRILLIC SMALL LETTER A>), never collapsed to a bare
_ - and pure ASCII unless you opted into Show mode's real glyphs. So it is safe to open in any
editor, unlike a normal terminal's raw log which can carry the very escapes this terminal refuses, and it
still records exactly what was there.
Signal keys reach the foreground program even in raw mode, and the Terminate action force-kills a program
that ignores everything. You are never stuck inside something you cannot get out of.
The OSC escapes that reach out of the grid - window title, desktop notification, clickable hyperlink,
clipboard write, clipboard read, palette change, working-directory report - are each a separate,
off-by-default toggle with a one-line note on what it exposes. (The iTerm2 OSC 1337 extensions have
no toggle - they can never be safely enabled, so they are always neutralised.) A three-part
security indicator dims from green toward red as you enable riskier features. When a program tries
to use a feature you left off, a dismissible banner tells you once per feature per tab - visible but
never nagging, and silenceable globally or per feature.
The same neutralization, for text you paste into other programs - an editor, a chat box, anything
that does not treat unicode as carefully. sclip is a tiny filter that reads text and writes it
back clipboard-safe: strictest by default (printable ASCII plus tab and newline, so an invisible, bidi or
homoglyph character cannot ride out), or --unicode to keep printable non-ASCII. It composes with
the system clipboard tools, and drop-in wrappers - st-wl-paste, st-wl-copy,
st-xclip - run the pipeline for you.
Or run it as a tray watcher (secure-terminal --clipboard-watch, no terminal window):
copy something deceptive and the same in-window review bar pops up and offers to replace it -
flag-and-offer, never a silent rewrite. You choose Keep original, Replace (ASCII), or Replace (keep
unicode). Honest limit: keeping unicode keeps printable look-alikes too (a homoglyph is printable
unicode); only the ASCII strip removes them, which is why the preview names every character so you can judge.
It sanitises the local clipboard; on Qubes the inter-VM global clipboard is untouched - run it after
pasting into the target VM.
You can also drive the watcher from an open terminal: View > Clipboard sanitizer (and the
right-click menu) start / stop it, toggle the warn-on-any-non-ASCII mode, review the clipboard on demand,
and set start-on-login. Since it is a tray app, turning the terminal's system tray off also turns its
autostart off.
The paste review - a curl | bash line hiding two Cyrillic
homoglyphs (in example and bash), a zero-width space and a right-to-left override.The bar names what is hidden at the top, then shows the paste in one editable box that opens
fully revealed - nothing is silently pre-dropped, so a trap cannot hide by being cleaned away before
you see it: each look-alike is tinted and named inline, and every invisible or reordering character is
shown in place as a named badge. While any hidden character remains, the Paste button is disabled
and red (blocked - N hidden) so nothing unreviewed can cross. Three transforms clear it - Strip
unicode deletes every non-ASCII character (the domain collapses to the visibly-broken
exmple.com), ASCII-fold folds each look-alike to the ASCII it imitates (revealing
example.com), and Keep printable unicode drops only the invisible/reordering
characters - or you edit the box directly; Restore original re-reveals it. Paste then enables,
still gated by an anti-fat-finger countdown. Rendered headless from the real bar.The same bar, for copy going the other way - selecting and copying that
same line in Show mode, before it reaches the system clipboard.Copy is reviewed with the same fully-revealed bar, relabelled for the other direction (the
reject button reads Don't copy). The box is the exact preview of what would land on the clipboard,
and the same Strip unicode / ASCII-fold / Keep printable unicode transforms clear it. The If accepted
guarantee is shown here too - for a copy it reads placed on the system clipboard (nothing runs), so
the row is consistent whichever direction you are in. There is no countdown - a copy is not executed, so
the anti-fat-finger gate the paste needs does not apply.Every risky feature explained in place - each escape-feature toggle carries
a selectable pop-up naming exactly what enabling it would expose.Unlike a plain hover tooltip this one is selectable and stays open, so the full risk note can
be read and copied; its colours follow the terminal's theme and its text scales with the zoom. The same
pop-up backs every entry in the granular escape-feature list above.
The strict default cannot run curses apps. TUI mode (per tab, off by default, clearly marked in the
toolbar and status bar) interprets escapes inside an isolated in-memory screen model (pyte) that has no OS
reach: it never sets the OS window title from output and cannot touch the clipboard. A program-set
window/tab title is shown, but quarantined on a separate, visibly-untrusted line of the tab
(plain ASCII, never reported back), so it can never pose as the trusted, app-set label. The clipboard,
hyperlink and notification side-effects are separate opt-in toggles (off by default) that dim a security
indicator when enabled, so those exfiltration/phishing vectors stay closed unless you deliberately open one.
Every character it places is still ASCII/unicode-filtered, and colours use a contrast guard so nothing can be painted invisibly.
What you give up: a program can draw a misleading interface within its own screen, so only run
programs you trust. This is "restricted-emulator safe," not "safe by design." The default CLI mode,
and everything the guarantee rests on, is unchanged.
# in TUI mode, verified end to end:ssh, vim, htop, tmux and apt's progress bar all render correctly,
with every cell still filtered, the clipboard off-limits, and any program-set title quarantined.
Named, colour-coded, each its own shell. Per-tab theme, zoom,
unicode mode and colours. Rename by double-click; a user name and a program title are kept separately.
Full ANSI colour -- 16-colour, 256-colour and 24-bit truecolor -- off by default, with a
contrast guard so text can never be invisible or hide itself against the background (a crafted red-on-red is forced legible). Honours NO_COLOR and TERM=dumb.
Tabs and their scrollback survive a restart or reboot, under a
fresh shell. Stored under ~/.local/state; on by default, and you can turn it off or clear it.
A standard Debian source package (Debian trixie). Pick a method; click to expand.
Run from a checkoutno install
Clone and run it in place, no packaging step:
git clone https://github.com/secure-terminal/secure-terminal
cd secure-terminal
./usr/bin/secure-terminal # the GUI
./usr/bin/secure-terminal-cli # the sanitizing CLI wrapper
Needs PyQt6 (and python3-pyte for TUI mode), both in Debian main.
Then run secure-terminal for the GUI, or secure-terminal-cli
to wrap a shell or command in the terminal you are already in.
Kicksecure repositorycoming soon
An apt install from the Kicksecure repository is planned, so updates
arrive with your normal system upgrades. Not available yet.
Kicksecurecoming soon
secure-terminal is planned to be installed by default in
Kicksecure, so a hardened system ships
with a safe terminal out of the box. Not available yet.
Yes. secure-terminal is Free Software under the AGPL-3+, developed in
the open on GitHub.
Read the source, build it yourself, audit exactly what it does.
Linux. It ships as a standard Debian source package (Debian trixie and
derivatives such as Kicksecure and Whonix); it is a PyQt6 application, so it
runs anywhere PyQt6 and a pseudo-terminal are available. The
secure-terminal-cli wrapper needs only Python and a POSIX pty.
Yes. It is written in Python (PyQt6); there is no C parser of
untrusted output to overflow. The whole point is that untrusted bytes meet a
small, auditable sanitiser, not a sprawling escape-sequence interpreter.
A normal terminal interprets whatever a program prints, including
attacker-controlled escape sequences, invisible characters and homoglyphs.
secure-terminal does not: by default it parses no escapes, shows every
character as itself (or a visible marker), and warns before a paste with
hidden characters reaches the shell. See the
comparison for the specifics.
Traditional terminal emulators have a live vulnerability class: merely viewing a file -
cat a log, tail a server's output - can make the terminal
type an attacker's text into your shell and run it, no keypress needed.
It is not a historical footnote: the same class recurs, most recently as
remote
code execution in iTerm2 in 2024 (CVE-2024-38395), one of the twenty-odd real cases the
terminal-poc-corpus collects.
secure-terminal closes the whole class by design - CLI mode runs no escape parser and answers no
query sequence, so output can never become input. See the comparison, measured
with screenshots.
output lies documents the problem in depth, with a live
analyzer, a
quiz, and real-world cases (Trojan Source, homoglyph
domains, pastejacking). secure-terminal is the tool that removes it at the source.