Skip to content

About

iOS SSH terminal client with agent forwarding, ProxyJump & Coder integration — WORK IN PROGRESS, vibe-coded

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

334 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

BicTerm

⚠️ WORK IN PROGRESS — NOT FULLY FUNCTIONAL

This project is under active development and is not ready for use. It was built using AI-assisted "vibe coding" — code was generated by AI agents working from a detailed plan, not hand-crafted by a human who understood every line. Treat it accordingly: there are bugs, untested edge cases, and rough edges. Review everything before relying on any of it.

An iOS 18+ SSH terminal client for iPhone and iPad, built on SwiftNIO SSH and SwiftTerm.

What Works

  • SSH connections with a key availability pool: every key you enable (ed25519 from Keychain, P-256 from Secure Enclave) is offered automatically on each connect attempt, the way an SSH agent works. The server picks from what it is offered; there is no per-connection key selection by default. A connection's Customize section can narrow the offer to an explicit set of keys, and switching Offer Keys off skips keys entirely. Passwords are decoupled from key selection: save a password connection blank to be prompted during connect or reconnect (RFC 4252 password only), and a saved or prompted password still completes authentication when the server rejects every key or requires both factors. The prompt can remember destination passwords in this device's Keychain, protected when locked and never transferred to another device; jump-host prompts are session-only (save hop passwords in the editor instead). Connection rows summarize the destination's offer as All keys (N offered), N selected keys, or Password, and the editor verifies the local Keychain entry before showing Saved on this device. Herdr connections authenticate through the same SSH layer. Key resolution is coalesced into one biometric evaluation per connect action (jump chains and herdr bring-ups included): every handshake of a single connect — the destination, each hop of a jump chain, each machine of a herdr bring-up — shares one resolution instead of prompting per handshake, and each new connect action (reconnect, re-open) evaluates fresh.
  • Key management with per-key switches: generate or import an SSH key, or copy a public key, directly from the connection editor's key picker; the picker reads the Keychain live and auto-selects a freshly saved key. The Key Management screen gives every key an on/off switch that controls whether it joins the default offer, and a warning appears once more than five keys are enabled (many servers allow only six authentication attempts, OpenSSH's default MaxAuthTries 6, and may disconnect before later keys are tried). The Settings hardware-keys default affects only the pool a connection inherits; a key you explicitly select in Customize always applies.
  • Discard confirmation in connection and hop editors — cancelling with uncommitted edits prompts "Discard Changes?" instead of silently dropping the draft; untouched drafts (or edits reverted to the saved values) dismiss instantly, and swipe-down dismissal is disabled while a draft is dirty
  • ProxyJump / jump chains up to 5 hops with per-hop host-key verification
  • TOFU host-key trust — fingerprint prompt on first connect, hard reject on changed keys
  • In-app SSH agent with per-request authorization, session cache, auto-deny when backgrounded
  • Terminal UI — SwiftTerm-based, hardware keyboard, IME/CJK composition, multi-window on iPad with freeform resizing (iPadOS 26 classifies the app as continuously resizable — drag the window's corner grip to any size or aspect ratio; declared via the orientation arrays in the xcodegen-generated BicTerm/Info.plist, guarded by BicTermUITests/FreeformResizeUITests.swift)
  • Multiple concurrent sessions — session switcher with detach/reattach that preserves terminal state, on iPhone and iPad. The scene's top-right session menu (ellipsis) lists every live session with its state for jump-to-session (on iPad it focuses the window already hosting the session, or gives a detached session its own window), opens the full switcher via Manage Sessions…, starts a New Session, and opens Settings… (its own window on iPad, a sheet on iPhone)
  • Graceful reconnect — network drops reconnect automatically; a clean remote shell exit stays disconnected until manual Retry. Background suspends live sessions and reopening the app keeps them reconnect-required until manual Retry (so app open never evaluates biometrics without a connect action); app relaunch requires manual reconnect. Reconnect resets terminal mouse, paste, and keyboard modes and exits the alternate screen while preserving normal scrollback. New connections reuse dead iPad terminal windows; active sessions keep separate windows.
  • Bell and OSC 777 notifications — BEL on SSH terminals is visible and audible (soundAndVisual: layer flash plus sound). Remote OSC 777 ; notify ; title ; body events render a dismissible banner in the sending scene for about five seconds (per-scene isolation, a newer event replaces the banner). While the app is not visible, at most one sanitized local notification per session per five seconds is posted opportunistically; backgrounding closes SSH transports, so no delivery is ever promised after suspension.
  • True color — SSH sessions advertise COLORTERM=truecolor to the remote PTY on both the direct and the ProxyJump path, so 24-bit color applications get full-depth output.
  • Confirmed links — tapping with a finger or Apple Pencil opens a confirmation sheet for both explicit OSC 8 hyperlinks and implicitly detected URLs, showing the full target; Open is enabled only for http and https links, and Cancel never opens anything. Trackpad and mouse clicks keep the existing hover-gated link behavior. Built on SwiftTerm fork hunks 13 (touch-type-aware activation) and 14 (semicolon-safe OSC 8 parsing); hunk 12 (hardware-keyboard word movement) predates them. The fork's patch ledger is Vendor/SwiftTerm/BICTERM-PATCH.md.
  • Multi-line paste preview — pasting text that contains line breaks into an SSH terminal first shows a preview of the captured content (line count plus a bounded excerpt) with Confirm and Cancel; the captured string, never a later pasteboard value, is what gets delivered. Sessions whose remote application enabled bracketed paste bypass the sheet and keep SwiftTerm's framing.
  • Startup-command presets — the connection editor offers Shell (empty), tmux (tmux new-session -A -s main), screen (screen -xRR main), and Custom presets over the typed startup command. The command is sent to the opened shell followed by Return after every connect and reconnect; it applies only to terminal sessions, must exist server-side, and the whole section is hidden while the connection uses herdr.
  • Keep Screen On — Settings → Terminal → Keep Screen On (default off) keeps the display awake while the app is frontmost; the choice persists across launches.
  • Snippets — named reusable commands, managed globally under Settings → Terminal → Snippets and offered per connection in the terminal scene's session menu (global plus current-connection snippets). Insert sends the exact command bytes without pressing Return; Run asks for confirmation of command and target, then sends the bytes plus Return exactly once.
  • iPad keyboard commands — a Session command menu routes ⌘N (New Session), ⌘W (Close Session, with the existing close confirmation), ⌘] and ⌘[ (next/previous session with wraparound), and ⌘, (Settings) to the focused terminal window only. While a Settings or herdr window holds focus the commands no-op, and chords the menu does not claim keep reaching the terminal.
  • App lock — an opt-in lock under Settings → Security requires owner authentication (biometrics with passcode fallback) when returning to the app. While locked, every window scene is covered by an opaque privacy cover, and the in-app SSH agent refuses to authorize signing until the lock is released.
  • Passwords AutoFill — SSH password fields (the connect prompt, the connection editor, and the hop editor) advertise password semantics to the system, so Passwords AutoFill suggestions and the Keychain save flow work on them.
  • Herdr — the real herdr 0.9.1 TUI client compiled for iOS and embedded in-process. Its surface renders through the vendored SwiftTerm view inside the BicTerm host; its protocol networking rides the BicTermCore SSH stack via a per-machine bridge socket (no OpenSSH subprocess on device); its exec consumers (installer steps, bridge relays) ride shared connections with dedicated-connection fallback on channel denial — Coder-style budget gateways permit one session channel per connection lifetime; its own native multi-machine sidebar drives Mode A and herd selection; its own input/clipboard/capability-query handling replaces the prior app-layer panes. Embed patch series in Vendor/herdr/EMBED-PATCHES.md; xcframework build in scripts/herdr-embed-core.sh (idempotent after scripts/herdr-server-fetch.sh + scripts/fixtures-up.sh).
  • Herdr in two modes — (A) a per-connection "Use Herdr" toggle that opens one machine's herdr workspace over that SSH connection, and (B) Herd mode: named herds of existing connections that seed the embedded client's machine catalog so its own sidebar selects / dials / reports health per machine (selection-driven surface interest, per-machine failure isolation, one transport link per machine over the same SSH bridge)
  • Multi-endpoint hardening — one aggregate reconnect budget across every machine in a workspace (no reconnect storms), parallel background detach (an N-machine herd suspends in one drain window), bounded per-machine surface caches, and a typed authentication-lost diagnostic that never auto-retries
  • Transport abstraction — SSH is one conformer; ET/mosh can be added later without touching session layers
  • Accessibility — VoiceOver labels on every icon-only control (session chrome, switcher, key management, herdr header) and on editor text fields; 44×44pt minimum touch targets on app-layer controls (the vendored SwiftTerm accessory strip excepted); editor validation arms only after a field is touched, so pristine blank forms show no red; SSH key fingerprints render full-length over two lines — the distinguishing tail is never middle-truncated or shrunk

Terminal mouse and clipboard

  • Mouse-aware SSH applications can request X10, normal (1000), button-motion (1002), or any-motion (1003) reporting, including SGR (1006). Primary touch/pointer presses, drags, and releases use the existing terminal input path. Pointer hover is reported only in any-motion mode. Vertical wheel and two-finger scrolling are translated into wheel-button reports.
  • With mouse reporting off, double-tap a word or long-press and choose Select, then drag the selection and choose Copy; the selection survives streaming output while reporting stays off. A mouse/trackpad primary-button drag starts a local selection. Hold Option to always force local selection — it is never passed through to the remote application, even one that captures the mouse. Shift also bypasses capture, unless the remote application explicitly requests Shift capture.
  • Paste is a local user action again, with bracketed-paste framing when the remote application requests it. Remote OSC 52 clipboard writes follow the app-side hardened policy: foreground-gated, capped at 100 KiB, controlled by a default-ON Settings toggle, with an attribution toast; OSC 52 reads stay denied unconditionally.
  • Touch copy/paste without a keyboard: double-tap a word (or long-press → Select, then drag) and choose Copy from the edit menu — verified on the iPhone simulator. While the software keyboard is sticky-hidden, the accessory strip's trailing control becomes a Paste button that routes through the same paste path as the edit menu: multi-line content presents the paste-preview confirmation first, single-line content delivers directly (with bracketed-paste framing when the remote requested it).
  • Simulator acceptance covers selection/copy/paste over SSH, SGR drag bytes, Option-forced local selection, and moving a real Vim cursor by tapping. Physical trackpad hover, wheel, and two-finger gestures still need device validation. Secondary/middle-button reporting and horizontal wheel reporting are not implemented. These changes apply to the SSH terminal; the herdr surface is the embedded real client (T7).
  • Validation boundary for everything in this README: features are validated on the simulator only. Physical pointer hover and wheel behavior, hardware-keyboard chords, and other device-only behaviors remain pending separate device authorization and have not been exercised on hardware.

Terminal toolbar

  • The esc/ctrl/tab/arrows accessory strip defaults OFF when a hardware keyboard is attached (the common iPad case, detected via GCKeyboard.coalesced) and ON when only the on-screen keyboard is available. The Terminal Toolbar item in the scene's top-right session menu (ellipsis) toggles it and shows the current On/Off state; the explicit choice is persisted and wins over the heuristic.
  • When shown, the strip participates in layout — the terminal shrinks by the strip's height, so it never covers the terminal's bottom row.

Terminal font size

  • Pinch-to-zoom on a terminal surface rescales that session's font live (9–32pt in 0.5pt steps), creating or updating its per-window override. Settings → Appearance → Font Size sets the persisted global default via slider with a monospace preview and Reset to Default. Global changes apply immediately to every session without a font override (the font change recomputes the grid and emits an SSH window-change to the remote pty). Terminal font size is deliberately independent of Dynamic Type — the terminal is a fixed character grid, not body text. The embedded herdr TUI rides the same SwiftTerm view at the same font size, so its character metrics match SSH terminal sessions exactly.

Appearance theme

  • Settings → Appearance → Theme offers System (the default — follows the device appearance), Dark, and Light. The choice is persisted and applies to every window without a theme override immediately, including sheets. Dark and Light are both first-class design-token palettes; the terminal surface's native background/foreground re-resolve live on any appearance change, while the 16 ANSI content colors keep their remote-output semantics untouched.

Per-window appearance and margins

  • Open the terminal's top-right session menu (ellipsis) → Appearance to override Theme, Font Size, or Margins for that session only. Rows show the effective value; Global means the session follows the corresponding Settings default live. Choose Global inside any row to reset just that property. A System theme override follows the device even when the global theme is Dark or Light.
  • Font Size offers 0.5 pt increase/decrease actions and Adjust Font Size… opens a slider. Pinching always starts from the displayed font and updates this override, never the global default. Other windows are not reflowed by a session's zoom.
  • Settings → Appearance → Margins sets the persisted global default: None (0 pt), Small (5 pt, initial default), Medium (10 pt), or Large (20 pt). The session menu offers the same scale. Margins apply along the terminal's sides and bottom and recompute the remote grid.
  • Overrides are keyed by the registry scene ID, not by the connection. They survive switching away, detaching, and reattaching during this app run, including moving the session to another window. They are removed when the session closes and are not restored across app relaunch; restored SSH snapshots start with global defaults. Settings opened in a separate iPad window keep the global theme rather than the terminal window's override.

What Doesn't Work Yet

  • No mosh or Eternal Terminal — architecture supports adding them, but they are not implemented in v1.
  • Pointer/touch routing for herdr TUI scenes and graphics scenes — triaged as future-phase work in Docs/HERDR-RELEASE-TRACEABILITY.md. (SSH-terminal OSC 8 links are covered by the confirmed-link flow above.)

What's Not In Scope (v1)

  • SSH agent forwarding for herdr endpoints — upstream herdr has no agent-forwarding concept, so there is no protocol path to forward into (documented research verdict; the in-app SSH agent still serves terminal sessions)
  • Keyboard-interactive auth (NIOSSH has no keyboard-interactive client; password and public-key only)
  • PIV / USB-C hardware-token support (deliberately deferred; the Settings hardware-keys toggle is the only forward-hook)
  • RSA keys / key export
  • A dedicated system sshd fixture for password testing (UI password tests use the in-process test server on port 18090)
  • SFTP/SCP or port forwarding
  • Terminal transcript/scrollback persistence
  • Analytics
  • ssh_config/known_hosts import

Architecture

BicTermCore (Swift package — no SwiftUI/UIKit)
├── SSH/          SSH transport, agent codec, ProxyJump
├── Transport/    TerminalTransport protocol + capability descriptors
├── Sessions/     Session registry, reconnect engine
├── Keys/         Keychain repo, OpenSSH parser, Secure Enclave
├── Trust/        Host-key TOFU verifier
├── Models/       Connection, Hop, SessionSnapshot, etc.
├── Persistence/  SwiftData stores (config, host keys, snapshots)
└── Herdr/        herdr transports, command builder, probe

BicTerm (iOS app)
├── App/          SwiftUI shell, scene manifest
├── Terminal/     SwiftTerm integration
├── Sessions/     Session UI, trust prompts
├── Connections/  Connection list + editor
├── Keys/         Key management UI
├── Agent/        Agent authorization UI
├── Herdr/        Embedded herdr workspace UI
├── Herds/        Herd editor UI
├── Settings/     App settings
└── Design/       Tokens, dual dark/light palettes

Dependencies

External tools the build, test, and fixture chain needs. SwiftTerm 1.20.0 and swift-nio-ssh 0.15.0 are vendored under Vendor/ and need no separate install; do not brew anything for them.

Tool Minimum version Why it's needed Install (macOS)
Xcode 26, plus the iOS 26.3 simulator runtime Builds the app; provides xcodebuild, xcrun, clang, dsymutil, and the simulators every script targets Mac App Store or developer.apple.com, then xcode-select --install
XcodeGen 2.x Generates BicTerm.xcodeproj from project.yml; required before any Xcode or xcodebuild run brew install xcodegen
Python 3 3.9+ (stdlib only) Runs the UDS forwarder (Fixtures/bin/uds-forward.py) and the readiness helpers inside fixtures-up.sh Ships with the Xcode Command Line Tools; Homebrew alternative: brew install python
Rust (rustup + cargo) stable, with targets aarch64-apple-ios and aarch64-apple-ios-sim Builds the herdr FFI core in scripts/build-herdr-core.sh and Vendor/herdr/check.sh. Both scripts override RUSTUP_HOME to the repo-local .build-artifacts/rustup, so add the targets with that env set: RUSTUP_HOME=.build-artifacts/rustup rustup target add aarch64-apple-ios aarch64-apple-ios-sim curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh (or brew install rustup-init + rustup-init)
cbindgen pinned by cargo install --locked Generates HerdrCore.h; build-herdr-core.sh installs it repo-locally into .build-artifacts/tools/ on first run No action needed
Zig 0.16.0 (aarch64-macos tarball, sha256-pinned) Builds the vendored libghostty-vt static library for iOS in scripts/herdr-vt-build.sh; downloaded repo-locally on first run — but only needed to regenerate the artifact, since a prebuilt libghostty-vt.a is committed under Vendor/herdr/embed/libghostty-vt/ No action needed
cargo-deny latest License and advisory policy checks in Vendor/herdr/check.sh cargo install cargo-deny
jq 1.6+ License inventory assembly in Vendor/herdr/check.sh brew install jq
OpenSSH (/usr/sbin/sshd, ssh, ssh-keygen), nc, curl system versions SSH fixtures on ports 12222/12223 Preinstalled on macOS; nothing to install
xcbeautify any (optional) Prettier xcodebuild output in test-core.sh and test-ui.sh; both fall back to raw logs when absent brew install xcbeautify
cargo-audit, cargo-fuzz latest (optional, hardening only) Advisory audits and the herdr fuzz targets under Vendor/herdr/herdr-ios-ffi/fuzz/ (fuzz needs a nightly toolchain). Neither is installed on the current dev machine, and nothing in the normal build/test chain requires them cargo install cargo-audit cargo-fuzz

What each feature needs

Feature Required tools
Basic app build (xcodegen generate, open Xcode, build) Xcode, XcodeGen
Test fixtures (scripts/fixtures-up.sh) Python 3 and the preinstalled OpenSSH/curl tools; for the herdr servers, the pinned prebuilt binary via scripts/herdr-server-fetch.sh (curl download, sha256-verified — never built from source)
herdr FFI build (scripts/build-herdr-core.sh) Rust with both iOS targets (cbindgen self-installs)
Regenerating the vendored iOS libghostty-vt (scripts/herdr-vt-build.sh) Zig 0.16.0 (self-downloads, sha256-verified); network on first run for the zig tarball and the pinned uucode 0.2.0 dependency
Hardening / SBOM (Vendor/herdr/check.sh, fuzz targets) cargo-deny, jq; cargo-audit and cargo-fuzz for the optional audit/fuzz passes

Not required: Docker (no container is used anywhere in the fixture flow). Zig is only needed to regenerate the vendored iOS libghostty-vt artifact (scripts/herdr-vt-build.sh); every normal build and test path uses the committed .a and needs no zig. The herdr server fixture is a pinned prebuilt release binary (herdr-macos-aarch64, v0.9.1) fetched and sha256-verified by scripts/herdr-server-fetch.sh; no test script builds the server from source.

Common-case install

brew install xcodegen python jq
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
RUSTUP_HOME=.build-artifacts/rustup rustup target add aarch64-apple-ios aarch64-apple-ios-sim
cargo install cargo-deny   # only needed for Vendor/herdr/check.sh

Verify your setup

xcodebuild -version      # Xcode 26+
xcodegen --version       # 2.x
python3 --version        # 3.9+
cargo --version          # rustup-managed stable
RUSTUP_HOME=.build-artifacts/rustup rustup target list --installed | grep apple-ios   # both iOS targets
jq --version
/usr/sbin/sshd -? 2>&1 | head -1   # system sshd present

Building

Install the tools above first: see Dependencies.

# Prerequisites: Xcode 26+, iOS 26.3 simulator runtime
xcodegen generate
open BicTerm.xcodeproj
# Scheme BicTerm, destination iPhone 17 Pro or iPad Pro 13-inch (M5).
# The herdr FFI core + embed xcframework must exist first:
scripts/build-herdr-core.sh   # builds .build-artifacts/herdr/HerdrCore.xcframework
scripts/herdr-embed-core.sh   # builds .build-artifacts/herdr/HerdrEmbed.xcframework

BicTerm.xcodeproj is generated by XcodeGen from project.yml and intentionally untracked — regenerate it after cloning or whenever project.yml changes (only its SwiftPM Package.resolved pin is tracked).

Testing

The fixtures and suites below need the tools listed under Dependencies.

# Fetch the pinned prebuilt herdr server (idempotent, sha256-verified),
# then start local fixtures (sshd on 12222/12223, UDS forwarder, and one
# herdr server per port — HERDR_SERVERS, default "12222 12223").
# HERDR_LOSSY also starts the lossy proxy the LossyProxySyncIntegrationTests
# require — without it those 2 tests fail with "Connection refused" on 12322.
scripts/herdr-server-fetch.sh
HERDR_LOSSY=12322:delay=80ms scripts/fixtures-up.sh

# Core unit/integration tests
scripts/test-core.sh

# UI tests
scripts/test-ui.sh

# Tear down fixtures
scripts/fixtures-down.sh

Vendored Libraries

  • swift-nio-ssh 0.15.0 (Apache 2.0) — vendored fork with agent-forwarding patches
  • SwiftTerm 1.20.0 (MIT) — vendored local fork; every BicTerm hunk (mouse/selection repair, reconnect mode reset, OSC 52 write surface, hardware-keyboard word movement, touch-activated links) is documented hunk-by-hunk in Vendor/SwiftTerm/BICTERM-PATCH.md
  • herdr 0.9.1 (Apache 2.0) — vendored protocol core + iOS FFI, embedded in-process in this app as the herdr TUI. The embed patch series lives under Vendor/herdr/embed-patches/ and is replayed by scripts/herdr-embed-prepare.sh; the resulting staticlib ships as HerdrEmbed.xcframework (built by scripts/herdr-embed-core.sh). Full ledger of every patch and provenance step: Vendor/herdr/EMBED-PATCHES.md. Runbook for moving the embed stack to a new herdr release: scripts/herdr-embed-update.sh (enforces the embed ABI contract; regenerates HerdrCore.h; rebuilds the xcframework).

See DEPENDENCIES.md for the full license inventory.

License

TBD — not yet licensed. All rights reserved until decided.

Disclaimer

This project was developed using AI-assisted code generation ("vibe coding"). The code was produced by autonomous AI agents executing a detailed plan. While it compiles, passes tests, and has been reviewed, it has not received the level of human scrutiny that hand-written production code would. Use at your own risk.

About

iOS SSH terminal client with agent forwarding, ProxyJump & Coder integration — WORK IN PROGRESS, vibe-coded

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages