slassh

iPhone & iPad · iOS 17 · coming to the App Store

ssh, with nothing in between.

A single-purpose SSH terminal for iPhone and iPad. It dials your machine directly — no relay, no account, no telemetry — and renders vim, tmux and journalctl the way your desk does.

  • · no account
  • · no relay
  • · no telemetry
  • · keys never leave the device
  • · dictation stays on it too

· One up-front price. No subscription, no in-app purchases.

edge-01 builder db-a
  1 use anyhow::Result;
  2 
  3 pub fn reconcile(s: &State) -> Result<Plan> {
  4     let mut plan = Plan::new();
  5     for svc in s.drifted() {
  6         plan.push(Step::Restart(svc.id));
  7     }
  8     // drift is settled; nothing else to do
  9     Ok(plan) 
 10 }
~
~
~
 reconcile.rs   rust  utf-8   9:18      -- INSERT --
 [ops] 0:vim* 1:logs 2:psql           edge-01 09:42 
ESCesc tab ^ctrl alt cmd mic
-~/|

Every screen on this page is a drawing.

Not a screenshot. slassh has no analytics SDK, no crash reporter and no screenshot service — so we have never seen a session, and we could not show you a real one if we wanted to.

What follows is the interface redrawn to the pixel in the app's own palette, running invented hosts and invented output. That is the whole product argument in one page: your terminal is not ours to display.

What it is for

Three jobs, done at full size.

A real VT emulator underneath — alternate screen, scrollback, SGR mouse reporting, bold, italic and underline, 24-bit colour. Programs do not find out they are talking to a phone.

vim opens. tmux attaches.
Nothing is approximated.

Full-screen programs get the alternate screen they expect, and the gesture you make on it goes to them — as SGR wheel reports when the program asked for the mouse, as arrow keys when it did not, which is what makes less and man scroll.

  • Every key reaches the shell. and -as-Meta included, so emacs chords survive the trip.
  • Ligature-free JetBrains Mono and a bundled Nerd Font, so a powerline prompt lands on the grid instead of shoving the row sideways.
  • 5 000 lines of scrollback on the primary screen, flung with one finger or paged with ⌘↑ / ⌘↓.
builder edge-01 ⌘1–9
dev@builder:~/src/reconcile$ cargo test --workspace
   Compiling reconcile v0.9.2 (/home/dev/src/reconcile)
    Finished `test` profile [unoptimized] in 12.44s
     Running unittests src/lib.rs

running 41 tests
test plan::drift_is_idempotent .............. ok
test plan::restart_orders_by_dependency ..... ok
test store::rejects_unknown_schema .......... ok
test store::round_trips_utf8_hostnames ...... ok
      … 36 more

test result: ok. 41 passed; 0 failed; 0 ignored

dev@builder:~/src/reconcile$  
 [dev] 0:vim 1:cargo* 2:ssh                 builder 14:08 

Keep an eye on the agent
from the platform.

A long run you started at your desk is a tail -f away from anywhere. The hard part on iOS is not the terminal — it is staying connected while you look at something else, and that is the part we spent the time on.

  • A glance away costs nothing. Switching apps takes a background task worth roughly half a minute — enough for a password manager and back.
  • Coming back, we ask the server, not the socket. A connection iOS froze still reads as healthy from inside the app, so slassh sends a keepalive and waits for the pong before trusting it. No answer, it redials and the tab carries on.
  • tmux on the far end is the durable answer. A dropped link then costs an attach, not the work — the layout, the scrollback and the running job are where you left them.
agent-box builder live
dev@agent-box:~$ tail -f .agent/run-4471.log
14:02:11  run    start   task=migrate-auth  model=opus
14:02:12  tool   read    src/auth/session.rs
14:02:19  tool   edit    src/auth/session.rs   +34 -12
14:03:04  tool   bash    cargo check
14:03:22  warn   2 warnings, 0 errors
14:04:10  tool   edit    tests/session.rs      +58  -0
14:06:41  tool   bash    cargo test --workspace
14:07:55  check  ok      41 passed, 0 failed
14:07:56  run    done    18 files · 6m 45s · $0.71

dev@agent-box:~$  

The pager goes off
and you are already in.

No laptop, no tethering, no VPN client to wake up first. Face ID on the key, the host key checked against the one you pinned, and you are at a prompt on the box that is unhappy.

  • Reachable one-handed. The modifier row puts ^, and under your thumb; then 19 switches hosts with no hardware keyboard anywhere.
  • The key can require your face. Per key, not per app — and enrolling a new face or finger re-locks it.
  • A changed host key stops the session. It shows you the fingerprint. It does not warn and continue.
edge-03 edge-01 02:14
ops@edge-03:~$ systemctl status caddy
× caddy.service — Caddy web server
     Active: failed (Result: exit-code) since 02:14:06
      Error: listen tcp :443: bind: address in use

ops@edge-03:~$ ss -lptn 'sport = :443'
State   Local Address:Port    Process
LISTEN  0.0.0.0:443           caddy,pid=811,fd=9

ops@edge-03:~$ sudo kill 811 && systemctl start caddy
ops@edge-03:~$ systemctl is-active caddy
active
ops@edge-03:~$  

Keyboard first

Nothing requires a tap.

Every screen — the host list, the settings, the import review, the key generator — is fully operable from a hardware keyboard. On an iPad in a case that means you never leave the home row. Without one, the on-screen modifier row reaches the same commands.

⌘T
Open the host browser in a new tab
⌘1⌘9
Jump straight to a session
⌘⇧[⌘⇧]
Cycle through open sessions
⌘↑⌘↓
Page the scrollback; typing snaps back to the prompt
⌘=⌘-⌘0
Text size, per your eyes and the light
⌘I
Import an existing ~/.ssh
⌘⌥K
Show or hide the on-screen keyboard
⌘D
Dictate; the same again sends what it heard
↑↓←→
Everything else goes to the remote, untouched
ESCesc tab ^ctrl alt cmd mic
-~/|
outlined — armed for the next key filled — locked until you tap it off mic — filled while it listens; the same switch as ⌘D every other key repeats while held

The row is one app-wide switch, and it survives a relaunch. Text fields keep their keyboard either way, so a password prompt still works with the row turned off.

Hands elsewhere

Say it, read it, then let it run.

iOS dictation cannot reach a terminal. It revises what it heard by replacing ranges of text, and there are no ranges here — only bytes already on the wire. So slassh brought its own recogniser, and the difference is the point: it buffers, so nothing is on the wire until you have read it.

⌘D, and it types
what you said.

The MIC key, the button in the tab bar and ⌘D are one switch, and all three light amber while the microphone is open. A banner across the top shows the transcript being revised as it settles.

  • Nothing reaches the shell until you stop. The partials are there to watch, not to send. Only the settled transcript goes, as a bracketed paste — which is what a chunk of text arriving at once is meant to be.
  • No Return unless you ask for it. It is a setting, and it is off, because a misheard word with a Return behind it is a command that ran.
  • On-device, or it does not happen. There is no server path to switch off and nothing to consent to: the model runs on the Neural Engine, and slassh installs it itself the first time you need it.
builder edge-01
docker compose logs -f api
dev@builder:~/deploy$ git pull --rebase
Successfully rebased and updated refs/heads/main.

dev@builder:~/deploy$  
ESCesc tab ^ctrl alt cmd mic

Hands-free, and it sleeps
between turns.

For when reading a line before it runs is not an option — walking, driving, mid-set at the gym. Press the mic once and it keeps listening until you say hi slash mic off.

  • Asleep, nothing it hears can reach the shell. The microphone really is running; it is listening for the wake phrase and for nothing else. The first version sent everything it heard on every pause, and a mic left on quietly posted a room's conversation into a live session. A phone that hears something and does nothing is the only safe resting state for this.
  • A pause ends the turn, not a word. Any spoken trigger has to survive recognition, and where this mode gets used — noise, music, out of breath — sometimes it will not, silently, because you said the word and nothing happened. A transcript that has not moved for two and a half seconds cannot be misheard. It is not a silence detector either: those places are never quiet, and only words move a transcript. Music does not.
  • Marked experimental, and it is the only thing here that is. It sends with Return, unread — a fair trade against an agent that asks before it acts, and a poor one at a bare prompt.
Asleep — say “hi slash” to wake it
restart the api container
grey — the mic is open and the shell cannot hear it amber — awake, and showing only what came after the phrase the rest was said to the room, and stays there
“hi slash”
Wakes it for one turn; what follows is what gets sent
“hi slash clear”
Throws away what you just dictated, and keeps listening
“hi slash mic off”
Sends what you said, then stops listening
“hi slash keyboard on”
Or off — the same switch as ⌘⌥K

Why the phrase is two words

One of them would have to be “slash”, and a slash is what you dictate at a terminal all day — every path, and Claude Code's own /clear and /compact. A single-word wake would swallow them.

Matching is on sounds, not spellings, because sounds are what comes back. A device transcript of “hi slash mic off, hi slash clear” arrived as:

Hi Celeste, Michael hi Celeste clear high/clear hello/

So the tables list what the recogniser actually returns — “celeste”, “michael”, “hello” — rather than what a dictionary would suggest. A path still cannot fire a command: the phrase needs a greeting in front of the slash, and cd /usr/local/bin has none.

Speech recognition makes mistakes. It mishears words, names and punctuation, and the shorter the phrase the likelier it is wrong — “mic off” has come back as “Michael”, “Mac off” and “my golf” on real devices. That is the argument for buffering, and against a Return you did not ask for. Dictation needs iOS 26; where the model cannot run, every microphone in the app is absent rather than disabled — no button, no MIC key, no ⌘D, nothing in Settings. A control that cannot work is worse than no control.

Setting up

Bring your ~/.ssh across in about a minute.

Most SSH apps make you paste keys in one at a time. slassh reads the config off the machine that already has it — over SSH, the same way you would yourself.

STEP 01

Point it at a machine you can already reach

A laptop running sshd, a bastion, anything. slassh looks in ~/.ssh, then ~/.config/ssh, and tells you on the review screen which one it actually read.

STEP 02

Read the review, tick what you want

Hosts, the keys they reference, Include fragments expanded in place. Anything that cannot come across is listed with the file and line it came from — nothing is dropped quietly.

STEP 03

Confirm, and only then is anything written

Private keys land in the Keychain, never in the Files app. Import the same config again later and it updates in place — hosts match on alias, keys on fingerprint.

The config parser is checked against ssh -G itself, so what slassh resolves is what ssh resolves — Match blocks, negated patterns, first-value-wins precedence and %h/%p expansion included.

Privacy, itemised

Claims you can check.

Privacy-first is easy to put on a page. Here is the specific version, phrased so that you could go and verify it. The formal version is DopeCode's privacy policy.

private keys
They live in the iOS Keychain, not in a file something else can be handed. Per key — not per app — an optional Face ID gate uses .biometryCurrentSet, so enrolling a new face or finger locks that key until you deliberately unlock it again. Generate ed25519 on the device and the private half never exists anywhere else.
the network path
One TCP connection, from your device to the host you named. No relay, no bastion service, no vendor in the middle — which also means a box on your LAN or behind your VPN is reachable exactly as it is from your laptop, and a box that is unreachable stays unreachable. There is no service here to go down, or to be subpoenaed.
host keys
Trust on first use, then pinned. A key that has changed stops the connection and puts the fingerprint in front of you; it does not warn and carry on. Bare ssh-rsa (SHA-1) is off the host-key algorithm list deliberately, spelled out rather than filtered, so a library update cannot quietly add one back.
what it sends
No analytics SDK. No crash reporter. No account. The only network traffic slassh makes is SSH, to a host you named, when you asked for it. Hosts and keys stay on the device they were added to — there is no sync, because there is nowhere for it to sync to.
what it hears
Dictation is transcribed on the device, or not at all. There is no server path in the app to fall back to and nothing to consent to — the language model is downloaded and run locally. The microphone is never held in the background, and leaving the app or switching tabs drops the utterance rather than committing it: half a spoken command is not something to send anywhere.
the shipping build
Development switches are compiled out of the release binary, not merely turned off. The shipped app does not link NSProcessInfo at all, so there is no code path in it that could accept a host key on its own, take a password out of the environment, or read a key off a filesystem path.
what it is made of
The transport (russh) and the terminal emulator (alacritty) are memory-safe Rust. The Swift side draws the cell grid and encodes your keystrokes — that is the whole of its job. No web view, no JavaScript, no bundled runtime.

The honest part

What it does not do.

Better you read this here than find it out after paying. slassh is one thing done properly, and the list of things it is not is short and deliberate.

Not in the app

  • No file transfer. No sftp, no scp browser.
  • No port forwarding or SOCKS proxy.
  • No ProxyJump or ProxyCommand. Connections are direct; hosts that need a jump import unticked rather than silently broken.
  • No mosh. It wants a mosh-server on every host you touch and a UDP range open to reach it, and it keeps no scrollback of its own. Run tmux on the far end instead and a dropped link costs an attach, not your work.
  • No telnet, no serial.
  • No sync between your devices, and no backup of your keys anywhere but your own.

Where iOS gets in the way

iOS has no background mode that means “I am holding a TCP connection”, and no app can invent one. Leaving slassh buys about half a minute of running time, then the system freezes the process and a frozen process answers no keepalives.

So: coming back, every session that was live is re-checked against the server and redialled if it has gone. There is no switch that holds the app awake indefinitely — the only way to build one is to play silence in the background, and that is not what the audio mode is for. Anyone promising otherwise is describing a relay, not a terminal.

The durable answer is not on the phone at all: run tmux on the far end and the shell lives on the machine iOS is not freezing. A dropped link costs an attach, and the layout, the scrollback and the running job are where you left them.

Help

The questions people ask.

How do I connect to my first machine?

Tap Add a Host and type the address the way you would at a terminal — root@server.example.com or pi@192.168.1.20:2222 fills in the host, user and port. Choose Password or SSH Key, save, and tap the host. The first time, slassh shows the server's key fingerprint and asks whether to trust it; from then on it stops you if that key ever changes.

How do I log in with a key instead of a password?

In the host, choose SSH Key and tap Generate a New Key. The key is made on the device and never leaves it. Then open it under SSH Keys — the key button on the Hosts screen — and tap Install on a Host: slassh logs in once with your password and adds the key to that server's ~/.ssh/authorized_keys.

A key you already have imports the same way: Import a Key, then paste it or pick the file. OpenSSH, PKCS#8 and PEM keys all work, with or without a passphrase.

Can I bring my ~/.ssh/config across?

Yes — + ▸ Import from ~/.ssh. The easy route reads it over SSH from a machine you can already reach: add your laptop as a host first (on a Mac, switch on Remote Login), then point the importer at it. You see every host and key before anything is saved.

It won't connect. What now?

“Refused the connection” means something answered, but not SSH on that port: check that SSH is running (on a Mac: System Settings ▸ General ▸ Sharing ▸ Remote Login) and that the port is right. “Didn't answer in time” means the device cannot reach the machine at all — usually a different network, or a VPN that is not on.

My password works from my laptop but not in slassh.

Check the user name first — it is the usual cause. slassh answers both ways servers ask for a password, including the keyboard-interactive prompt of FreeBSD and many PAM setups. It cannot answer a second factor such as a one-time code; use an SSH key there.

My session drops when I switch to another app.

iOS gives every app about half a minute in the background, then pauses it, and a paused app cannot answer the server. Coming back, slassh checks each session and reconnects the ones that dropped. Run tmux on the server and a drop costs one tmux attach: the layout, the scrollback and the running job are all still there.

I got a new phone. Where are my keys?

On the old one, deliberately: keys are kept out of backups and cannot be restored onto another device. Generate a new key on the new phone and install it on your servers. One key per device means a lost phone costs one line in authorized_keys, not a new key everywhere.

Why is there no microphone button?

Dictation runs entirely on the device, so it needs iOS 26 and hardware that can run Apple's on-device speech model. Where that is missing, slassh leaves the microphone out rather than showing a button that cannot work.

Still stuck? Write to hello@dopecode.com with your slassh version (Settings ▸ About) and what the error said. Never send keys or passwords — we will never ask for them.

iPhone & iPad · App Store

One price, once.

slassh is a one-time purchase. No subscription, no in-app purchases, no tier that unlocks the feature you actually needed. It has nothing to sell you afterwards, which is the point.

  • one-time purchase
  • no subscription
  • no in-app purchases
  • iPhone & iPad · iOS 17+

· Questions first? Help answers the common ones.