Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Skiff icon

Skiff

A tiny iOS SSH terminal that hops you home to your own tmux — pure client, no relay, owns nothing.

Platform Swift License

English · 简体中文

Skiff is a small SwiftUI terminal for driving a long-running tmux session on a machine you own — the desktop at home doing the heavy lifting — from your phone. It is built for the case where the computer runs the work and the phone is the remote control.

It is deliberately not a general-purpose SSH client. It does one route well: phone → (optional single jump box) → your host → attach tmux.

Note: the in-app UI text is currently Chinese only. Everything else — code, comments, docs — is English. Localization is welcome (see Contributing).

Connection screen    Terminal with the custom key row    Command panel

Design rules

These are the constraints the implementation is held to. They are the reason the app is this small:

  1. Pure client, no relay. No third-party cloud, no daemon to install on the host. Just SSH.
  2. tmux owns the session. Skiff never takes over the session lifecycle — a laptop can ssh + tmux attach into the exact same session at any time, with zero config changes.
  3. One jump hop, max. Phone → your public jump box → the host behind NAT. Arbitrary hop chains are out of scope.
  4. Agent-agnostic. Whatever CLI you run is just a process inside tmux. The app is a dumb PTY forwarder and stays blind to it.
  5. Baseline usability is non-negotiable. Smooth scrollback, correct CJK/wide-character rendering, and a real Ctrl/Esc/Tab/arrow key row.
  6. Notifications are optional and orthogonal. They ride on the terminal bell, not on a push service you have to sign up for.

Features

SSH Pure-Swift SwiftNIO SSH. PTY + tmux new -A, correct UTF-8/CJK.
Auth Device ed25519 key held in the Keychain, offered first; password fallback only if the target rejects the key and advertises password auth. The jump hop is key-only — there is no jump password field. The public key is shown on the connect screen with a copy button.
Host keys Trust-on-first-use pinning, keyed by host:port — so two reverse tunnels on the same jump box don't share a pin. Mismatch aborts the connection.
ProxyJump Single hop, implemented as a direct-tcpip channel on the jump connection carrying a second, independent SSH handshake — the jump box never sees the inner session's plaintext.
Session picker Lists real tmux sessions via tmux ls; expand one to see its windows and attach straight into a specific window. Or create a new session by name.
Structure save / rebuild Save a named snapshot of a host's session+window names, and rebuild that skeleton with one tap after the host reboots. Rebuild recreates names only — it does not restore process state, by design.
Terminal UX Touch-drag scrollback (mapped to mouse wheel, needs tmux mouse mode), pinch-to-zoom font size (9–28pt, persisted), one-finger horizontal swipe to switch tmux windows and two-finger swipe to switch tmux sessions, tappable window tab strip.
Key row Custom accessory row replacing SwiftTerm's default: esc, ⌃B (tmux prefix), tab, `
Command panel Editable queues of one-tap command chips (each optionally auto-pressing Return). Fully user-configurable in-app; JSON-persisted.
Notifications A remote bell (\a) becomes a local notification — no APNs, no paid account, no third-party push service. Have your long job ring the bell when it finishes.
Background Silent-audio keepalive holds the SSH connection while the app is backgrounded. It engages automatically whenever an SSH terminal is live and stops on return to the foreground — there is no user-facing toggle yet. Mixes with other audio, so it won't interrupt your music.

Requirements

  • Xcode 16 or newer, iOS 16+ target (developed and tested against Xcode 26.5)
  • XcodeGen (brew install xcodegen) — project.yml is the source of truth; Skiff.xcodeproj is generated and not committed
  • Dependencies are declared as from: version floors in project.yml, so a fresh checkout resolves to current upstream releases. Pin them there if you need reproducible builds.
  • A host running sshd and tmux

Build and run

git clone https://github.com/DKmiyan/skiff-ios.git
cd skiff-ios
xcodegen generate            # project.yml -> Skiff.xcodeproj
open Skiff.xcodeproj

In Xcode, set your own signing team on both targets, then Run. Swift Package dependencies resolve on first build.

To build and test from the command line:

xcodebuild -project Skiff.xcodeproj -scheme Skiff \
  -destination 'platform=iOS Simulator,name=iPhone 17 Pro' build

xcodebuild -project Skiff.xcodeproj -scheme Skiff \
  -destination 'platform=iOS Simulator,name=iPhone 17 Pro' test

Substitute any simulator you actually have installed for iPhone 17 Pro.

Signing. project.yml ships with the placeholder bundle id com.example.skiff and no development team. Set DEVELOPMENT_TEAM and your own reverse-DNS PRODUCT_BUNDLE_IDENTIFIER in project.yml (then re-run xcodegen generate), or just pick your team in Xcode's Signing pane. A free Apple ID works for device installs, but the certificate expires after 7 days and needs re-signing.

Xcode 26 downloads the iOS platform and the Metal toolchain separately — xcodebuild -downloadPlatform iOS and xcodebuild -downloadComponent MetalToolchain. SwiftTerm has Metal shaders and will not build without the latter.

Host setup

1. Authorize the app's key. Launch Skiff, copy the public key shown at the bottom of the connect screen, and append it to ~/.ssh/authorized_keys on the host (and on the jump box, if you use one). The key is generated on first launch when the Keychain has none, and reused after that. Simulator and device have separate keychains, so each needs its own authorized_keys entry.

2. Direct connection. Fill in host, port, user. Leave the password blank to force key auth.

3. Via a jump box. Turn on ProxyJump and fill in the jump host/port/user. The target host field is evaluated from the jump box's point of view. So for a host behind NAT that maintains a reverse tunnel to the jump box:

# On the host behind NAT — expose its port 22 as port 2201 on the jump box:
ssh -N -R 2201:localhost:22 jumpuser@jump.example.com

then in Skiff set jump = jumpuser@jump.example.com:22 and target = youruser@127.0.0.1:2201. Because host-key pins are keyed by host:port, a second tunnel on 127.0.0.1:2202 is correctly treated as a different machine.

4. Optional — finish notifications. Make whatever long-running job you care about ring the terminal bell on completion, e.g. printf '\a' in a completion hook. Skiff turns that into a local notification (suppressed while in the foreground, shown when backgrounded).

5. Recommended — enable tmux mouse mode so touch-drag scrollback works:

echo 'set -g mouse on' >> ~/.tmux.conf

Architecture

Sources/
├── SkiffApp.swift            app entry
├── ContentView.swift         stage machine: connect -> session picker -> terminal
├── ConnectView.swift         connection form, recent servers, public-key display
├── SessionPickerView.swift   tmux session/window list, save & rebuild structure
├── Theme.swift               dark IDE-ish palette
├── WindowTabBar.swift        tmux window tab strip
├── CommandStore.swift        command queue model + persistence
├── CommandPanelView.swift    command panel sheet + editor
├── SessionHistory.swift      recent session names
├── SessionStructureStore.swift  named session/window skeletons
├── LastConnectionStore.swift    last + recent connections (never passwords)
├── SkiffNotifications.swift  bell -> local notification, de-duplicated
├── BackgroundKeepAlive.swift silent-audio background keepalive
├── SSH/
│   ├── SSHConnector.swift    transport: direct or ProxyJump (nested SSH over direct-tcpip)
│   ├── SSHSession.swift      interactive half: PTY + tmux, streams to the terminal
│   ├── SSHSessionLister.swift one-shot `tmux ls` / `list-windows` query
│   ├── SSHAuth.swift         key-first, password-fallback auth delegate
│   ├── SSHKeyStore.swift     Keychain-backed ed25519 identity
│   └── HostKeyStore.swift    TOFU host-key pinning, keyed by host:port
└── Terminal/
    ├── TerminalSession.swift the transport seam the terminal talks to
    ├── TerminalScreen.swift  SwiftTerm bridge
    ├── WheelTerminalView.swift touch scroll -> wheel, pinch zoom, swipe windows
    ├── SkiffKeyBar.swift     custom key accessory row
    └── EchoSession.swift     offline local-echo session for self-check

SSHConnector is shared by the interactive session and the one-shot tmux ls query, so both take the exact same connect path (including the jump hop and host-key checks).

Security notes

  • Authentication prefers the Keychain ed25519 key. A password is only offered if the server rejects the key and advertises password auth; it is never written to disk.
  • Saved connections store host/port/user only. Passwords are never persisted.
  • Host keys are pinned on first use and verified on every later connect; a mismatch fails the connection rather than prompting. To re-pin a genuinely changed host key you currently have to clear the app's stored pin (delete and reinstall the app).
  • TOFU means the first connection is trusted blindly. Over an untrusted network, make that first connection somewhere you trust, or verify the fingerprint out of band.
  • The jump hop carries a second, independent SSH session inside a direct-tcpip channel, so the jump box cannot read the inner session.

Status and limitations

Working and used daily: SSH direct + ProxyJump, key auth with host-key pinning, session/window picker, structure save & rebuild, scrollback, swipe/pinch gestures, key row, command panel.

Rough edges, honestly:

  • Bell notifications and background keepalive are lightly tested on physical devices.
  • No paste button yet; no in-app UI to reset a host-key pin.
  • One jump hop only, by design — no multi-hop chains.
  • No mosh; a dropped connection means reconnecting (though tmux means you lose nothing).
  • UI strings are Chinese-only.

Contributing

Issues and PRs are welcome — especially English/other localization, device-testing reports for the notification and keepalive paths, and a host-key re-pin UI. Please keep the design rules above intact: no relay component, and tmux keeps owning the session.

Credits

Built on SwiftTerm (terminal emulation) and swift-nio-ssh (SSH). Prior art worth reading: SwiftTermApp, Citadel.

License

MIT

About

A tiny iOS SSH/tmux terminal that hops you home to your own machine — pure client, no relay, tmux owns the session

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages