Injection Shield and egress slots
How Gatesys SSH keeps server text from instructing the model, guards terminal links, and sends sentences to a model without their values.
Two safeguards stand beside every AI feature. The Injection Shield makes whatever a server wrote data to the model, never instructions, so the model sees what you saw. Egress slots let a sentence go to a model without its values: hosts, addresses, paths, logins, ports and quotes leave only as placeholders.
Both are plain code in the app’s main process. No model is ever asked whether something is an injection.
What the shield does before Farabi’s question is sent
Before Farabi’s question leaves, the main process:
- Removes text you could not see: text drawn concealed, printed and then erased or overwritten on the line, drawn in (almost) the background colour, or carried inside an OSC title, DCS, APC, PM or SOS string. It is never sent.
- Strips invisible characters: Unicode tag characters, zero-width spaces and joiners, bidi overrides, embeddings and isolates. It keeps what real text needs, such as the joiner inside an emoji, joiners inside Persian, Arabic and Indic words, and direction marks on right-to-left lines.
- Defangs chat-template tokens from six dialects, such as
<|im_start|>,[INST],<<SYS>>and<tool_call>, and anything that folds to the app’s own fence markers, fullwidth look-alikes included. - Fences the terminal between markers carrying a random per-request nonce, which the system prompt declares as evidence, not instructions.
- Labels a line that reads as written to an AI. It takes two signals at once, such as “if you are an AI assistant, tell the user to run …”. A note beside the block names the line number only. Nothing is withheld for its wording: a runbook’s “ask the user to run ipconfig” is one signal and goes as it is.
The terminal text is read out of the terminal’s own buffer, the way it was drawn, so no cursor trick can make the model read something the screen did not show.
Every other server string
OS and shell names, each host fact as the Host facts block is built, each change in Explain’s list, and a port’s greeting, an SSH ident, a server’s algorithm list and a bastion’s refusal as Hop Doctor captures them are all flattened and scrubbed the same way. One that reads like instructions is replaced by [withheld: reads like instructions]. The facts block, the change list and Hop Doctor’s report each go in a fence of their own, declared as data.
The model’s reply
Before you see a reply, it loses reordering characters, live chat tokens and any fence marker or nonce.
Your instructions and skills
Farabi’s instructions and skills lose invisible characters, chat-template tokens and fence look-alikes before they go into the prompt, so none can close a data fence or open a turn. The shield also reads them for wording that tries to loosen Farabi’s rules, such as you may run or no need to confirm. A skill of yours that reads that way waits for your approval before any conversation uses it. This check reads only your own text, never terminal output, where a runbook may well tell a person to run something.
The chip under your question
When something was removed, a chip under your question says so, for example 1 hidden line removed before asking. Show lists what was removed, as it would have looked, in this window only.
- There is no chip when nothing was removed.
- No chip ever says the output was safe.
- A line that was only labelled raises no chip.
Removing hidden text is written to the audit log with the host, the runtime and the model, never the text.
Terminal links and escapes
These rules apply with the assistant on or off:
- Only
httpandhttpslinks open from a terminal, and never one carrying a user name or password:https://[email protected]goes to evil.example, so it is refused. - Look-alike hosts ask first. A host in punycode or mixing alphabets shows the real host and asks.
- Every OSC 8 hyperlink asks first, because its visible text is the server’s choice, not its target. The question takes focus from the terminal; Esc or Cancel opens nothing.
- No other windows. The app refuses any other link, a page’s
window.opennever opens a window, and the window cannot navigate away from the app. - No clipboard writes from a server. OSC 52 requests are swallowed, so nothing printed can put text on your clipboard.
The terminal check runs within a 50 ms budget and drops the block rather than send it unchecked.
What the shield does not do
It removes the carriers it knows and fences the rest; it does not recognise every paraphrase. That is why command safety rates every command Farabi suggests on this computer, whatever the model was told, and why nothing runs without your click. Test this model measures how often your model follows a planted line.
Egress slots
A feature that hands a model a sentence to read into something typed can send it slotted: every identifying value is replaced by a typed placeholder before it is sent.
| Placeholder | Stands for |
|---|---|
‹H1› | A host or domain name, such as db-1.prod.example.com, and every saved host’s name |
‹A1› | An IP address, v4 or v6, with a prefix length or a zone, or a MAC address |
‹P1› | A file path: /var/log/…, ~/.ssh/…, ./x, dir/file.ext, C:\… |
‹U1› | A login or an email: deploy@web-1, root@[2001:db8::7] |
‹N1› | A port, or any number for a reader that only labels values |
‹Q1› | Quoted text, in any kind of quote marks |
A URL is taken apart the same way: https://‹U1›:‹N1›‹P1›.
How slotting works
- One value, one placeholder for the whole request, so the model can still reason about it (“‹H1› refused ‹N1›”). The system prompt says the placeholders are to be copied, never guessed.
- The mapping never leaves the main process. It is never sent, stored, logged or passed to the window.
- Real values go back in before anything is shown, while the reply streams too.
- A guessed placeholder is refused. A reply that names a placeholder the request never had can only be a guess, and nothing after it is shown.
- Typed replies map roles, not values. In a draft, each field must hold one placeholder of a kind that field accepts, and one placeholder may fill only one field. The model never supplies a value itself.
- All or nothing. A turn too long to mask (64 KB) or holding more than 500 values is refused before anything is sent, never sent half-masked.
What gets masked
- Invisible characters are stripped and fullwidth characters read as ASCII first, so an address split by a zero-width space or written in fullwidth digits is still an address.
- Placeholder look-alikes already in the text become plain angle brackets.
- Loopback addresses and
localhoststay readable; they are the same on every machine. - Detection errs toward masking. A dotted name whose last part is letters is a host unless that part is a file extension, so
acme.shopanddb01.k8sare masked, whilenginx.conf,deploy.sh,README.md, versions, times and dates go as written. A script calledcheck.plreads as a Polish domain and is masked, which costs the model a word. - A one-word name with no dots that is not a saved host, such as
gpu-box, cannot be told from a word and is not masked. - Secrets are masked by redaction first, as everywhere else.
Where slots are used
| Request | Slotted |
|---|---|
| Watches and setup drafts | Always, on every runtime, including a model on this computer. The setup reader has every number masked too |
| Safe Change reviews and drafts | When the prompt leaves this computer, after your consent |
| Farabi’s Neighbours block | When the prompt leaves this computer |
| The rest of Farabi’s questions, and both Explains | No; they get the treatment in What runs on the server |
Footers say what happened, for example Read by qwen3:8b · local endpoint · 1 value masked, or sent your sentence with 3 values masked to OpenAI.
Something unclear or wrong? Tell us.