Files
tty7/docs/remote/ssh.mdx
T
l0ng-ai 6fcf659e04 feat(search): tabbed Search Everywhere with a Sessions tab to resume agent sessions (#969)
* feat(search): replace the command palette with tabbed Search Everywhere

The palette was one flat list that every new kind of row had to be squeezed
into: tabs and SSH hosts rode along as "Switch to Tab: …" and "SSH: …"
commands, and the only way to narrow to one kind was a magic seed word.

Search Everywhere splits it into sources behind one trait — All, Actions,
Terminals, Hosts — each with its own empty-query layout and ranking. The All
tab shows each source's top rows, ordered by best match, with a row that
opens the full tab. Tab / Shift-Tab walk the tabs and keep the query.

- Terminals lists every open tab of every workspace (reusing the switcher's
  tab rows) and jumps to it wherever it lives, plus the shells and agents.
- Hosts replaces the separate "Add Connection" input: a typed address or
  full `ssh …` line offers to connect.
- Fixes Return doing nothing after a search that found nothing, or when the
  search opens pre-filtered: gpui-component re-picks the row from a stale
  frame; the delegate now re-arms the first row.
- Fixes `ssh -p 2222 me@box` being offered as a quick-connect address with
  user `ssh -p 2222 me`.

The keymap action stays `TogglePalette` so custom bindings keep working.

* feat(search): a Sessions tab to resume past agent sessions

Search Everywhere gains a Sessions tab listing the Claude Code and Codex
sessions on this computer, read from ~/.claude/projects and
~/.codex/sessions ($CODEX_HOME). Sessions that ran in the focused tab's
directory lead; the All tab offers the last three of them before anything
is typed. Return opens a new tab in the session's directory and runs the
agent's resume command with its configured launch flags.

- Only each transcript's head and tail are read, off the window thread,
  and cached by path, size and mtime: ~170ms cold for 50 sessions,
  under 1ms warm. The search opens on the cached list and fills in.
- Titles: /rename name, then the agent's own title (ai-title, Codex's
  session_index.jsonl), then the first thing typed, skipping harness
  injections. Codex rollouts it ran for itself (subagent/internal) and
  sessions never asked anything are left out.
- A session whose directory is gone is refused with a notice rather than
  resumed where the agent cannot find it.
2026-09-27 09:38:31 +08:00

217 lines
7.8 KiB
Plaintext

---
title: "SSH"
description: "A native Rust SSH stack: quick connects, saved profiles, keychain credentials, jump hosts."
---
tty7 speaks SSH itself, over [russh](https://github.com/Eugeny/russh). It never
shells out to the `ssh` binary, and there is no compatibility mode that does.
That is what makes the rest possible: credentials in the OS keychain,
[SFTP](/remote/sftp) in a side panel, [port forwards](/remote/port-forwarding)
you can add mid-session, and authentication prompts drawn as sheets in the pane
instead of a password echoing into your shell.
<Frame caption="A key passphrase asked for as a sheet inside the pane, with the option to keep it in the keychain">
<img src="/images/ssh-passphrase.webp" alt="Connecting over SSH in tty7" />
</Frame>
## Four ways to connect
<AccordionGroup>
<Accordion title="QuickConnect — type an address">
Open Search Everywhere (<kbd>⌘ P</kbd>) and type an address. IPv6 works with
brackets.
```
me@devbox
me@devbox:2222
me@[2001:db8::1]:22
```
</Accordion>
<Accordion title="A saved profile">
Profiles live in **Settings → SSH → Hosts**. Start typing the name in
Search Everywhere's Hosts tab, or open the *SSH: Manage Profiles…* command.
</Accordion>
<Accordion title="An alias from ~/.ssh/config">
Type an alias you already have and tty7 resolves it natively — common fields,
best effort — then connects over russh. **Settings → SSH → Import from
~/.ssh/config** turns aliases into real profiles.
<Note>
`Match`, `canonicalize*`, and GSSAPI directives are not supported, and
there is no fallback to the system `ssh` when one appears.
</Note>
</Accordion>
<Accordion title="A remote workspace">
The same connection can host whole workspaces on the far machine rather than
a single shell. [Remote workspaces →](/remote/workspaces)
</Accordion>
</AccordionGroup>
## Profiles
**Settings → SSH → Hosts** holds the full connection config. The basics:
| Field | |
|---|---|
| **Name** | A label for this connection |
| **Host** | Hostname or IP |
| **User** | Login user — blank resolves at connect time |
| **Auth** | *Auto* (tries every applicable method), *GSSAPI*, *Password*, *Key*, *Agent*, or *2FA* |
| **Jump host** | Another profile, or a `ProxyJump` chain |
| **Port forwarding** | Rules opened with the connection |
**Defaults** at the top of the list is inherited by every host, so a setting you
want everywhere is set once.
Right-clicking an SSH tab opens that connection's host form — **Edit Host…**
for a saved one, **Save as SSH Host…** for an address typed by hand. It is the
same row the workspace switcher's machine menu carries, so a hostname or
password typed wrong is corrected from the tab you noticed it on. The menu acts
on the tab it was opened on, not on whichever pane is focused. Saving one opens
on the whole live connection — its proxy, keys and forwards as well as its
address — so the host that lands is the one you were already on.
Saved hosts are kept in `servers.json`, beside `config.json` rather than in
it, so a `config.json` you sync between machines does not carry them.
Passwords and key passphrases go in the **OS keychain**, never in either file
and never on disk in plain text. **Forget Password** in a
profile's menu removes the stored one.
Deleting a profile drops its keychain credentials and forgets the remote
workspace entries that connected through it — the confirmation counts them
first. The sessions on the machine itself keep running; [what happens to its
entries →](/remote/workspaces#deleting-a-profile)
### Advanced
Behind **Advanced** on a profile, grouped:
| Group | Fields |
|---|---|
| **Authentication** | Identity files (one path per line, `%h`/`%r` expand), agent forwarding |
| **Proxies** | ProxyCommand (`%h`/`%p`/`%r` substituted), SOCKS5 proxy, HTTP proxy |
| **Algorithms** | KEX algorithms, ciphers, MACs, host-key algorithms, compression |
| **Connection** | Keepalive interval and count, connect timeout, X11 forwarding |
| **Session** | Shell integration, login scripts, skip banner |
| **Security** | Host-key verification, remote clipboard image writes |
Everything blank means "the library default", so you only fill in what you
actually need to override.
## Copying a remote image to this machine
Programs on an SSH host can write PNG, JPEG, GIF, or WebP images to the system
clipboard on the machine running tty7 with the OSC 5522 clipboard protocol.
Enable **Advanced → Security → Remote clipboard images** for that saved host
first. It is off by default because any program that writes terminal output
would otherwise be able to replace the clipboard.
This Python script can be installed on the remote host as
`tty7-copy-image`:
```python
#!/usr/bin/env python3
import base64
import os
import pathlib
import re
import select
import secrets
import sys
import termios
import time
import tty
path = pathlib.Path(sys.argv[1])
mime = {
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".webp": "image/webp",
}.get(path.suffix.lower())
if mime is None:
raise SystemExit("supported formats: png, jpg, jpeg, gif, webp")
data = path.read_bytes()
if len(data) > 16 * 1024 * 1024:
raise SystemExit("image exceeds tty7's 16 MiB clipboard limit")
osc, st = b"\x1b]5522;", b"\x1b\\"
encoded_mime = base64.b64encode(mime.encode())
request_id = secrets.token_hex(8)
out = sys.stdout.buffer
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
status = None
try:
tty.setraw(fd)
rid = request_id.encode()
out.write(osc + b"type=write:id=" + rid + st)
for offset in range(0, len(data), 4096):
chunk = base64.b64encode(data[offset:offset + 4096])
out.write(
osc + b"type=wdata:id=" + rid + b":mime=" + encoded_mime + b";" + chunk + st
)
out.write(osc + b"type=wdata:id=" + rid + st)
out.flush()
reply = bytearray()
pattern = re.compile(
rb"\x1b\]5522;type=write:status=([A-Z]+):id=" + rid + rb"\x1b\\"
)
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
ready, _, _ = select.select([fd], [], [], deadline - time.monotonic())
if not ready:
break
reply.extend(os.read(fd, 4096))
match = pattern.search(reply)
if match:
status = match.group(1).decode()
break
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old)
if status != "DONE":
raise SystemExit(f"clipboard write failed: {status or 'timeout'}")
```
Run `tty7-copy-image screenshot.png`. A compliant sender may include an OSC
5522 request id and wait for tty7's `DONE`, `EPERM`, `EINVAL`, or `ENOSYS`
response. Clipboard control packets are not retained in scrollback and are not
replayed after reconnecting.
## Authentication prompts
Password, key passphrase, and 2FA prompts appear as sheets inside the pane, with
a **Remember (keychain)** option where it makes sense.
## Host keys
Host keys are verified against `known_hosts` by default. A first connection asks
you to confirm the fingerprint; a **changed** key is a much louder prompt that
makes you type `yes` to override, because that is what a changed key deserves.
**Settings → SSH → Security → Verify host keys** turns verification off
entirely. It is on for a reason.
Also under Security: **Warn before closing** a live connection, off by default.
## Reconnecting
<kbd>⌘ ⇧ R</kbd> — or *SSH: Reconnect* in Search Everywhere — restarts the session in
the current pane. Useful after a laptop sleeps or a network changes.
## What is not supported
- No fallback to the system `ssh` binary
- No `Match` or `canonicalize*` directives from `~/.ssh/config`
- No GSSAPI *directives* from `~/.ssh/config`. Kerberos `gssapi-with-mic` itself
is supported — pick **GSSAPI** in a profile's Auth field — it is just not
something the config-file resolution path reads