gritty
Persistent, self-healing terminal sessions over SSH for smooth remote development.
gritty gives you seamless, robust remote shell sessions that survive network changes, laptop sleep, and SSH disconnects. Close your laptop, change networks, reconnect your VPN -- gritty detects the dead connection, respawns the SSH tunnel, and picks up where you left off. gritty also optionally forwards your ssh-agent and will handle remote requests to open URLs via $BROWSER.
It works by forwarding Unix domain sockets over SSH -- no custom protocol, no open ports, no certificates, no configuration. If you can ssh to a host, you can use gritty for reliable remote development.
Features
- Reliable
- Self-healing connections -- heartbeat detection, automatic tunnel respawn, transparent client reconnect
- Persistent sessions -- shells survive client disconnect, network failure, laptop sleep
- Remote development
- SSH agent forwarding (
-A) -- tunnels your local SSH agent sogit push,ssh, and other agent-dependent commands work remotely - URL open forwarding (
-O) -- forwards$BROWSER/ URL open requests back to your local machine - Environment forwarding -- TERM, LANG, COLORTERM propagated to remote shell
- SSH agent forwarding (
- Simple
- Single binary, zero config -- optional TOML config for defaults; no server config, no port allocation, no root required; auto-starts the remote server
- No network protocol -- Unix domain sockets locally, SSH handles encryption and auth
- Session management
- Multiple named sessions -- create, list, attach, kill by name or ID
- SSH-style escape sequences --
~.detach,~^Zsuspend,~?help
Quick Start
Connect to a remote host
One command sets up an SSH tunnel, starts the remote server, and returns:
Create sessions, attach, detach, reattach -- all through the tunnel:
# Create a named session (auto-attaches)
# Detach with ~. or just close your terminal
# Reattach from any terminal
# Forward your SSH agent for git/ssh on the remote host
# Forward URL opens back to your local browser
# List sessions
# Manage tunnels
gritty ls devbox output:
ID Name PTY PID Created Status
0 work /dev/pts/4 48291 2026-02-21 14:32:07 attached (heartbeat 3s ago)
1 deploy /dev/pts/5 48305 2026-02-21 14:33:41 detached
Commands
| Command | Aliases | Description |
|---|---|---|
gritty connect user@host |
c |
Set up SSH tunnel to remote host |
gritty disconnect <name> |
dc |
Tear down an SSH tunnel |
gritty tunnels |
tun |
List active SSH tunnels |
gritty new-session [host] [-t name] |
new |
Create a session and auto-attach |
gritty attach [host] -t <id|name> |
a |
Attach to a session |
gritty tail [host] -t <id|name> |
t |
Read-only stream of session output |
gritty list-sessions [host] |
ls, list |
List sessions |
gritty kill-session [host] -t <id|name> |
Kill a session | |
gritty kill-server [host] |
Kill the server and all sessions | |
gritty open <url> |
Open a URL on the local machine (inside sessions) | |
gritty socket-path |
socket |
Print the default server socket path |
gritty info |
Show diagnostics (version, config, server status, tunnels) | |
gritty config-edit |
Open config in $VISUAL/$EDITOR (creates from template if missing) |
The [host] argument is a connection name from gritty connect (e.g., gritty ls devbox). Omit it to use the local server.
Notable options:
-A/--forward-agentonnew/attach: forward your local SSH agent-O/--forward-openonnew/attach: forward URL opens to local machine-t <name>onnew/attach/tail: target session by name or ID-n <name>onconnect: override connection name (defaults to hostname)-o <option>onconnect: extra SSH options (repeatable, e.g.,-o "ProxyJump=bastion")--no-redrawonnew/attach: don't send Ctrl-L after connecting--no-escapeonnew/attach: disable escape sequence processing
Configuration
gritty works out of the box with no config file. Optionally, you can set persistent defaults in $XDG_CONFIG_HOME/gritty/config.toml (default: ~/.config/gritty/config.toml). Run gritty config-edit to create and open the config file.
# Global defaults for all sessions/connections.
[]
= false
= true
= false
# Connect-specific global defaults.
[]
= []
= false
# Per-host overrides, keyed by connection name.
# Connection name = hostname from destination, or -n override.
[]
= true
= true
[]
= ["IdentityFile=~/.ssh/devbox_tunnel_key"]
[]
= true
= true
[]
= true
Configurable settings: forward-agent, forward-open, no-escape, no-redraw (session), ssh-options, no-server-start (connect).
Precedence: CLI flag > [host.<name>] > [defaults] > built-in default. CLI flags always win. For ssh-options, values are appended: CLI first, then host, then defaults (SSH uses first-match, so earlier options take priority).
Host resolution: The [host.<name>] key matches the gritty connection name -- what appears in gritty tunnels and gritty disconnect <name>. For local sessions (gritty new without a host), only [defaults] applies.
A missing or malformed config file is silently ignored -- gritty remains zero-config if you want it to be. Use gritty info to check config status.
Escape Sequences
After a newline (or at session start), ~ enters escape mode:
| Sequence | Action |
|---|---|
~. |
Detach from session (clean exit, no auto-reconnect) |
~^Z |
Suspend the client (SIGTSTP) |
~? |
Print help |
~~ |
Send a literal ~ |
Design
No Network Protocol
gritty contains zero networking code. Sessions live on Unix domain sockets. For remote access, you forward the socket over SSH -- the same SSH that already handles your keys, your .ssh/config, your bastion hosts, your MFA.
No ports to open, no firewall rules, no TLS certificates, no authentication system to trust beyond the one you already use.
Security by Composition
gritty delegates encryption and authentication to SSH rather than reimplementing them. Locally, the socket is 0600, the directory is 0700, and every accept() verifies the peer UID. The attack surface is small because there's very little to attack.
Single-Socket Architecture
All communication -- control messages and session relay -- flows through one server socket. When a client connects to a session, the server hands off the raw connection and gets out of the loop. No per-session sockets, no port allocation, no cleanup races.
Persistence Model
The PTY and shell process keep running when the client disconnects. While disconnected, the server drains PTY output into a userspace ring buffer (1MB cap) so the shell never blocks -- long builds complete in the background. On reconnect, buffered output is flushed to the new client. There's no scroll-back replay or screen reconstruction -- just a live PTY that never dies.
How It Works
Architecture
flowchart LR
subgraph local["Local Machine"]
T1["Terminal"] <--> C1["gritty client"]
T2["Terminal"] <--> C2["gritty client"]
C1 <--> LS["connect-host.sock"]
C2 <--> LS
end
LS <-->|"SSH tunnel<br/>(single TCP connection)"| D
subgraph remote["Remote Host"]
D["gritty daemon<br/>ctl.sock"]
D -.->|handoff| S0 & S1 & S2
S0["Session 0 'work'<br/>● attached"] <--> P0["PTY + bash"]
S1["Session 1 'deploy'<br/>● attached"] <--> P1["PTY + bash"]
S2["Session 2 'docs'<br/>○ disconnected"] <--> P2["PTY + bash"]
end
linkStyle 0,1,2,3 stroke:#2080c0
linkStyle 4 stroke:#e07020,stroke-width:2px
linkStyle 5,6,7 stroke:#2080c0,stroke-dasharray:5 5
linkStyle 8,9,10 stroke:#2080c0
Orange = SSH tunnel (TCP) · Blue = Unix domain socket
A daemon listens on a single Unix socket (ctl.sock). Clients send a control frame declaring intent (new session, attach, list); the daemon hands off the raw socket connection to the target session and gets out of the loop. Each session owns a PTY with a login shell that persists across disconnects -- while no client is attached, the server drains PTY output into a userspace ring buffer (1MB cap) so the shell never blocks. On reconnect, buffered output is flushed to the new client.
For remote access, gritty connect forwards the remote socket over SSH. All commands work identically over the tunnel.
Self-Healing Reconnect
sequenceDiagram
participant C as gritty client
participant T as SSH tunnel
participant S as Session + PTY
C->>S: Ping (every 5s)
S->>C: Pong
Note over C,T: Network interruption
C--xS: Ping (no response)
Note over C: 15s with no Pong
rect rgb(255, 245, 245)
Note over C: [reconnecting...]
Note over S: Shell keeps running
C--xT: connect (tunnel down)
Note over T: Monitor detects exit,<br/>respawns SSH
C->>T: connect (tunnel back)
C->>S: Attach
S->>C: Ok
end
Note over C: [reconnected]
C->>S: Resize + Ctrl-L redraw
Note over S: Buffer drains,<br/>shell resumes
The client pings every 5 seconds; no pong within 15 seconds means dead connection. The client enters a reconnect loop (retry every 1s, Ctrl-C to abort). Meanwhile, the tunnel monitor detects the SSH process exit and respawns it. The client reconnects through the restored tunnel transparently.
Agent & URL Forwarding
flowchart LR
subgraph remote["Remote Host"]
direction LR
agent_sock["agent-N.sock<br/>(SSH_AUTH_SOCK)"]
open_sock["open-N.sock<br/>(GRITTY_OPEN_SOCK)"]
S["gritty session"]
agent_sock <-->|"-A"| S
open_sock -->|"-O"| S
end
S <-->|"relayed over<br/>session connection"| C
subgraph local["Local Machine"]
C["gritty client"]
ssh_agent["ssh-agent"]
browser["browser"]
C <--> ssh_agent
C --> browser
end
Forwarding multiplexes over the existing session connection -- no extra tunnels.
SSH agent (-A): the session creates agent-N.sock and sets SSH_AUTH_SOCK. When a remote process (e.g. git push) connects, the request is relayed to the client's local SSH agent and back.
URL open (-O): the session creates open-N.sock and sets GRITTY_OPEN_SOCK + BROWSER=gritty open. When gritty open <url> runs, the URL is relayed to the client which opens it locally.
Prior Art
- mosh -- persistent remote terminal using UDP and SSP
- Eternal Terminal -- persistent SSH sessions over a custom protocol
- tmux / screen -- terminal multiplexers with session persistence
gritty differs by having no network protocol of its own. Where mosh and ET implement custom transport and encryption, gritty uses Unix domain sockets and delegates networking entirely to SSH. Where tmux and screen are full multiplexers with windows, panes, and key bindings, gritty does one thing: persistent sessions with auto-reconnect.
Status & Roadmap
Early stage. Works on Linux and macOS. Available on crates.io.
Planned:
- Server auto-start -- start the server on demand (systemd socket activation, launchd, or on first
new-session) - Zero-downtime upgrades -- server re-execs itself, preserving sessions across upgrades
License
MIT OR Apache-2.0