sshmd 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sshmd-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 revsearch
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
sshmd-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,309 @@
1
+ Metadata-Version: 2.4
2
+ Name: sshmd
3
+ Version: 0.1.0
4
+ Summary: Cross-platform SSH session manager with a background daemon
5
+ Keywords: ssh,session-manager,daemon,port-forwarding,socks,terminal
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Classifier: Environment :: Console
9
+ Classifier: Intended Audience :: System Administrators
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: System :: Networking
13
+ Classifier: Topic :: Utilities
14
+ Requires-Dist: click>=8.0
15
+ Requires-Python: >=3.12
16
+ Project-URL: Homepage, https://github.com/revsearch/sshm
17
+ Project-URL: Repository, https://github.com/revsearch/sshm
18
+ Project-URL: Issues, https://github.com/revsearch/sshm/issues
19
+ Description-Content-Type: text/markdown
20
+
21
+ # sshm
22
+
23
+ [![PyPI](https://img.shields.io/pypi/v/sshmd)](https://pypi.org/project/sshmd/) [![Tests](https://github.com/revsearch/sshm/actions/workflows/tests.yml/badge.svg)](https://github.com/revsearch/sshm/actions/workflows/tests.yml)
24
+
25
+ An SSH session manager with a background daemon. Remote shells stay alive when you
26
+ close the terminal, reconnect on their own when the link drops, and reattach
27
+ instantly. State is kept in plain `~/.ssh/config`, so `ssh`, `scp`, and `rsync`
28
+ keep working alongside it.
29
+
30
+ ```bash
31
+ sshm add prod root@192.0.2.10 # keygen + copy key + write ~/.ssh/config
32
+ sshm prod # attach to a live shell (or start one)
33
+ # close the terminal — the shell keeps running; `sshm prod` reattaches it
34
+
35
+ ssh prod # plain ssh/scp/rsync work too — same alias & key
36
+ ```
37
+
38
+ ## Install
39
+
40
+ The package is published as **`sshmd`** and installs the `sshm` (and `sshmd`)
41
+ commands. With [uv](https://docs.astral.sh/uv/) or [pipx](https://pipx.pypa.io/):
42
+
43
+ ```bash
44
+ uv tool install sshmd
45
+ # or
46
+ pipx install sshmd
47
+ ```
48
+
49
+ Latest from git instead of PyPI:
50
+
51
+ ```bash
52
+ uv tool install git+https://github.com/revsearch/sshm
53
+ ```
54
+
55
+ From a checkout:
56
+
57
+ ```bash
58
+ git clone https://github.com/revsearch/sshm
59
+ cd sshm
60
+ uv tool install . # or: pip install .
61
+ ```
62
+
63
+ Needs Python 3.12+. The `sshm` and `sshmd` commands are installed to your tool bin
64
+ directory (`~/.local/bin`, or the Python Scripts dir on Windows) — make sure it's
65
+ on your PATH. Update with `uv tool upgrade sshm`.
66
+
67
+ ## Quick start
68
+
69
+ ```bash
70
+ # Add a server: generates an ed25519 key, copies it to the remote, writes config
71
+ sshm add myserver root@192.168.1.100
72
+
73
+ # Connect (interactive shell via the daemon)
74
+ sshm myserver
75
+
76
+ # Or explicitly
77
+ sshm c myserver
78
+ ```
79
+
80
+ ## Works with `ssh`, `scp`, `rsync`
81
+
82
+ `sshm add` writes a normal `Host` entry to `~/.ssh/config`, so the alias works with
83
+ any tool that reads it — not just `sshm`. The generated key and port are picked up
84
+ automatically, so no `-i`/`-p` flags are needed:
85
+
86
+ ```bash
87
+ ssh myserver # plain SSH, same alias
88
+ ssh myserver htop # run a one-off command
89
+ scp ./backup.tar.gz myserver:/srv/ # copy a file up
90
+ scp myserver:/var/log/app.log . # ...and back down
91
+ rsync -avz ./site/ myserver:/var/www/ # sync a directory
92
+ sftp myserver # sftp, git over ssh, etc. all work too
93
+ git clone myserver:/srv/repo.git # alias works as the ssh host anywhere
94
+ ```
95
+
96
+ The difference: `sshm myserver` attaches to a persistent, auto-reconnecting shell
97
+ via the daemon, while `ssh myserver` is a plain one-off connection — both to the
98
+ same host, using the same key and config.
99
+
100
+ ## Commands
101
+
102
+ Most commands have a short alias (shown first). `sshm <alias>` with no command is
103
+ shorthand for `connect`.
104
+
105
+ ### Sessions
106
+
107
+ ```bash
108
+ sshm <alias> # Connect (shorthand)
109
+ sshm c, connect <alias> [name] # Attach to a session or create a new one
110
+ sshm l, list # List all hosts
111
+ sshm l, list <alias> # List active sessions for a host
112
+ sshm a, add <alias> user@host # Add a server (keygen + copy key)
113
+ sshm r, remove <alias> # Remove a host and disconnect all sessions
114
+ sshm mv, rename <alias> <new> # Rename an alias (and its managed key)
115
+ ```
116
+
117
+ `add` takes `user@host`, `user@host:port`, or bracketed IPv6
118
+ (`user@[2001:db8::1]:22`).
119
+
120
+ ### Port forwarding and SOCKS
121
+
122
+ Forwards are written as native `LocalForward` / `RemoteForward` / `DynamicForward`
123
+ directives in `~/.ssh/config`, so any SSH client sees them. Direction is `-L` /
124
+ `-R` / `-D`, same as `ssh`:
125
+
126
+ ```bash
127
+ sshm p a, port add <alias> -L <local>:<host>:<remote> # Local forward
128
+ sshm p a, port add <alias> -R <remote>:<host>:<local> # Reverse forward
129
+ sshm p a, port add <alias> -D <port> # SOCKS proxy (ssh -D)
130
+ sshm p r, port remove <alias> -L <local>:<host>:<remote> # Remove a forward
131
+ sshm p r, port remove <alias> -D <port> # Remove a SOCKS proxy
132
+ ```
133
+
134
+ `-D <port>` is a dynamic forward — a SOCKS5 proxy on `127.0.0.1:<port>` tunnelled
135
+ through the host. Point a browser or any SOCKS-aware app at it.
136
+
137
+ ### Auto-connect
138
+
139
+ When enabled, the daemon keeps at least one shell alive and reconnects it on
140
+ failure, so attaching is instant.
141
+
142
+ ```bash
143
+ sshm e, enable <alias> # Keep a session alive, auto-reconnect
144
+ sshm d, disable <alias> # Stop auto-connect
145
+ ```
146
+
147
+ ### Import / export
148
+
149
+ Move hosts, including their keys, between machines as JSON.
150
+
151
+ ```bash
152
+ sshm export servers.json # Export all hosts
153
+ sshm export prod.json web db api # Export specific hosts
154
+ sshm l servers.json # Preview a JSON file
155
+ sshm import servers.json # Import (skip existing)
156
+ sshm import servers.json -o # Import (override existing)
157
+ sshm import servers.json web db # Import only specific hosts
158
+ sshm import servers.json web=prod # Import 'web' under the alias 'prod'
159
+ ```
160
+
161
+ ### Daemon
162
+
163
+ The daemon (`sshmd`) starts on first use.
164
+
165
+ ```bash
166
+ sshm status # Daemon status
167
+ sshm stop # Stop the daemon
168
+ sshm install # Autostart on login (systemd / launchd / Task Scheduler)
169
+ sshm uninstall # Remove autostart
170
+ ```
171
+
172
+ ## Shell completions
173
+
174
+ ### fish
175
+
176
+ `sshm` ships fish completions: subcommands, `port -L/-R/-D` flags, and host aliases
177
+ pulled live from `~/.ssh/config` (including the bare `sshm <alias>` shorthand).
178
+ `uv tool install` does **not** wire these up automatically, so install them once
179
+ (`exec fish` reloads the shell so they're active immediately):
180
+
181
+ ```fish
182
+ sshm completions fish > ~/.config/fish/completions/sshm.fish && exec fish
183
+ ```
184
+
185
+ fish autoloads from that directory — new sessions pick it up with no `source`. From
186
+ a checkout you can instead symlink the source file so edits are picked up live:
187
+
188
+ ```fish
189
+ ln -s (path resolve src/sshm/completions/sshm.fish) ~/.config/fish/completions/sshm.fish
190
+ ```
191
+
192
+ ## Session states
193
+
194
+ | Icon | State | Meaning |
195
+ |------|----------|-----------------------------------|
196
+ | `●` | ready | Shell running, waiting for attach |
197
+ | `◆` | attached | A client is connected |
198
+ | `○` | dead | Process exited |
199
+
200
+ ## How it works
201
+
202
+ ```
203
+ sshm (CLI) ── TCP localhost:19222 ──> sshmd (daemon)
204
+ ├── SSH processes (one shell per session)
205
+ ├── Reader threads (scrollback buffer)
206
+ ├── Watchdog (health, reconnect, keep-warm)
207
+ └── IPC server (JSON protocol + I/O bridge)
208
+ ```
209
+
210
+ - The daemon spawns `ssh -tt <alias>` under a real PTY (POSIX; pipes on Windows)
211
+ and holds the shell process.
212
+ - `connect` bridges your terminal I/O to that process over TCP and forwards your
213
+ terminal size (and resizes / `SIGWINCH`) so the remote shell matches your window.
214
+ - Detaching (closing the terminal, Ctrl-C) leaves the shell running; reattach later.
215
+ - Typing `exit` in the shell removes the session cleanly — no reconnect.
216
+ - A lost connection (SSH exit 255) triggers reconnect with exponential backoff.
217
+ - Config stays in `~/.ssh/config`. Every rewrite goes to a temp file and is
218
+ replaced atomically (a `config.bak` is kept), so a crash mid-write won't corrupt it.
219
+ - Runtime state lives in `~/.sshm/`: pid file, IPC token, `sshmd.log`.
220
+ - The IPC server binds `127.0.0.1` only and checks a random per-daemon token
221
+ (stored `0600` in `~/.sshm/token`) on every request.
222
+ - The IPC port defaults to `19222`; set `SSHM_PORT` to change it (e.g. to run sshm
223
+ in both Windows and WSL when mirrored networking shares localhost). `sshm install`
224
+ persists the port to `~/.sshm/port` so the autostarted daemon — which doesn't see
225
+ your shell env — uses the same one.
226
+
227
+ ## Platforms
228
+
229
+ - Linux — systemd user service for autostart.
230
+ - macOS — launchd LaunchAgent.
231
+ - Windows — Task Scheduler for autostart, Win32 console VT mode for terminal I/O.
232
+
233
+ On POSIX, ssh runs under a real PTY, so the remote shell tracks your window size
234
+ and resizes. Windows has no `pty` module, so there a session keeps the size it
235
+ attached with — full-screen apps (`vim`, `htop`) won't follow later resizes.
236
+
237
+ ## Development
238
+
239
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the full branch / commit / PR workflow.
240
+
241
+ ```bash
242
+ uv sync # install dependencies (including the dev group)
243
+ uv run pytest # run the tests
244
+ ```
245
+
246
+ Module layout (`src/sshm/`):
247
+
248
+ | Module | Responsibility |
249
+ |----------------|-----------------------------------------------------------|
250
+ | `cli.py` | CLI commands (click), entry point `sshm` |
251
+ | `daemon.py` | `sshmd`: request dispatch, watchdog, entry `sshmd` |
252
+ | `process.py` | SSH sessions: PTY spawn, scrollback, reconnect, health |
253
+ | `ipc.py` | TCP IPC client/server on `127.0.0.1:19222` (token auth) |
254
+ | `protocol.py` | JSON message schema for IPC |
255
+ | `config.py` | `~/.ssh/config` parser/writer, port-forward rules |
256
+ | `terminal.py` | Raw terminal bridge (termios on Unix, Win32 console API) |
257
+ | `procutil.py` | Cross-platform process helpers (pid checks, Popen flags) |
258
+ | `state.py` | `~/.sshm` runtime files: pid, token, port, log |
259
+ | `autostart.py` | Task Scheduler / systemd / launchd integration |
260
+
261
+ ## Troubleshooting
262
+
263
+ ### Windows: `Bad owner or permissions on ~/.ssh/config`
264
+
265
+ OpenSSH on Windows refuses to read `~/.ssh/config` if the ACL contains extra
266
+ principals like `OWNER RIGHTS` (S-1-3-4). Symptom:
267
+
268
+ ```
269
+ Bad permissions. Try removing permissions for user: \\OWNER RIGHTS (S-1-3-4) on file C:/Users/<you>/.ssh/config.
270
+ Bad owner or permissions on C:\\Users\\<you>/.ssh/config
271
+ ```
272
+
273
+ Remove the offending principal:
274
+
275
+ ```powershell
276
+ icacls "$env:USERPROFILE\.ssh\config" /remove "OWNER RIGHTS"
277
+ ```
278
+
279
+ If other files in `.ssh` have the same issue (private keys, etc.):
280
+
281
+ ```powershell
282
+ icacls "$env:USERPROFILE\.ssh\*" /remove "OWNER RIGHTS"
283
+ ```
284
+
285
+ If that's not enough (inheritance can pull in other groups), reset the ACL so only
286
+ you have access:
287
+
288
+ ```powershell
289
+ $f = "$env:USERPROFILE\.ssh\config"
290
+ icacls $f /inheritance:r
291
+ icacls $f /grant:r "$($env:USERNAME):(F)"
292
+ ```
293
+
294
+ If `icacls` returns `Access is denied`, you don't own the file. Take ownership
295
+ first, then fix the ACL:
296
+
297
+ ```powershell
298
+ takeown /F "$env:USERPROFILE\.ssh\config"
299
+ icacls "$env:USERPROFILE\.ssh\config" /grant "$($env:USERNAME):F"
300
+ icacls "$env:USERPROFILE\.ssh\config" /inheritance:r
301
+ icacls "$env:USERPROFILE\.ssh\config" /grant:r "$($env:USERNAME):(F)"
302
+ ```
303
+
304
+ If `takeown` fails, run PowerShell as Administrator and repeat. Verify with
305
+ `icacls "$env:USERPROFILE\.ssh\config"` — only your user with `(F)` should remain.
306
+
307
+ ## License
308
+
309
+ MIT — see [LICENSE](LICENSE).
sshmd-0.1.0/README.md ADDED
@@ -0,0 +1,289 @@
1
+ # sshm
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/sshmd)](https://pypi.org/project/sshmd/) [![Tests](https://github.com/revsearch/sshm/actions/workflows/tests.yml/badge.svg)](https://github.com/revsearch/sshm/actions/workflows/tests.yml)
4
+
5
+ An SSH session manager with a background daemon. Remote shells stay alive when you
6
+ close the terminal, reconnect on their own when the link drops, and reattach
7
+ instantly. State is kept in plain `~/.ssh/config`, so `ssh`, `scp`, and `rsync`
8
+ keep working alongside it.
9
+
10
+ ```bash
11
+ sshm add prod root@192.0.2.10 # keygen + copy key + write ~/.ssh/config
12
+ sshm prod # attach to a live shell (or start one)
13
+ # close the terminal — the shell keeps running; `sshm prod` reattaches it
14
+
15
+ ssh prod # plain ssh/scp/rsync work too — same alias & key
16
+ ```
17
+
18
+ ## Install
19
+
20
+ The package is published as **`sshmd`** and installs the `sshm` (and `sshmd`)
21
+ commands. With [uv](https://docs.astral.sh/uv/) or [pipx](https://pipx.pypa.io/):
22
+
23
+ ```bash
24
+ uv tool install sshmd
25
+ # or
26
+ pipx install sshmd
27
+ ```
28
+
29
+ Latest from git instead of PyPI:
30
+
31
+ ```bash
32
+ uv tool install git+https://github.com/revsearch/sshm
33
+ ```
34
+
35
+ From a checkout:
36
+
37
+ ```bash
38
+ git clone https://github.com/revsearch/sshm
39
+ cd sshm
40
+ uv tool install . # or: pip install .
41
+ ```
42
+
43
+ Needs Python 3.12+. The `sshm` and `sshmd` commands are installed to your tool bin
44
+ directory (`~/.local/bin`, or the Python Scripts dir on Windows) — make sure it's
45
+ on your PATH. Update with `uv tool upgrade sshm`.
46
+
47
+ ## Quick start
48
+
49
+ ```bash
50
+ # Add a server: generates an ed25519 key, copies it to the remote, writes config
51
+ sshm add myserver root@192.168.1.100
52
+
53
+ # Connect (interactive shell via the daemon)
54
+ sshm myserver
55
+
56
+ # Or explicitly
57
+ sshm c myserver
58
+ ```
59
+
60
+ ## Works with `ssh`, `scp`, `rsync`
61
+
62
+ `sshm add` writes a normal `Host` entry to `~/.ssh/config`, so the alias works with
63
+ any tool that reads it — not just `sshm`. The generated key and port are picked up
64
+ automatically, so no `-i`/`-p` flags are needed:
65
+
66
+ ```bash
67
+ ssh myserver # plain SSH, same alias
68
+ ssh myserver htop # run a one-off command
69
+ scp ./backup.tar.gz myserver:/srv/ # copy a file up
70
+ scp myserver:/var/log/app.log . # ...and back down
71
+ rsync -avz ./site/ myserver:/var/www/ # sync a directory
72
+ sftp myserver # sftp, git over ssh, etc. all work too
73
+ git clone myserver:/srv/repo.git # alias works as the ssh host anywhere
74
+ ```
75
+
76
+ The difference: `sshm myserver` attaches to a persistent, auto-reconnecting shell
77
+ via the daemon, while `ssh myserver` is a plain one-off connection — both to the
78
+ same host, using the same key and config.
79
+
80
+ ## Commands
81
+
82
+ Most commands have a short alias (shown first). `sshm <alias>` with no command is
83
+ shorthand for `connect`.
84
+
85
+ ### Sessions
86
+
87
+ ```bash
88
+ sshm <alias> # Connect (shorthand)
89
+ sshm c, connect <alias> [name] # Attach to a session or create a new one
90
+ sshm l, list # List all hosts
91
+ sshm l, list <alias> # List active sessions for a host
92
+ sshm a, add <alias> user@host # Add a server (keygen + copy key)
93
+ sshm r, remove <alias> # Remove a host and disconnect all sessions
94
+ sshm mv, rename <alias> <new> # Rename an alias (and its managed key)
95
+ ```
96
+
97
+ `add` takes `user@host`, `user@host:port`, or bracketed IPv6
98
+ (`user@[2001:db8::1]:22`).
99
+
100
+ ### Port forwarding and SOCKS
101
+
102
+ Forwards are written as native `LocalForward` / `RemoteForward` / `DynamicForward`
103
+ directives in `~/.ssh/config`, so any SSH client sees them. Direction is `-L` /
104
+ `-R` / `-D`, same as `ssh`:
105
+
106
+ ```bash
107
+ sshm p a, port add <alias> -L <local>:<host>:<remote> # Local forward
108
+ sshm p a, port add <alias> -R <remote>:<host>:<local> # Reverse forward
109
+ sshm p a, port add <alias> -D <port> # SOCKS proxy (ssh -D)
110
+ sshm p r, port remove <alias> -L <local>:<host>:<remote> # Remove a forward
111
+ sshm p r, port remove <alias> -D <port> # Remove a SOCKS proxy
112
+ ```
113
+
114
+ `-D <port>` is a dynamic forward — a SOCKS5 proxy on `127.0.0.1:<port>` tunnelled
115
+ through the host. Point a browser or any SOCKS-aware app at it.
116
+
117
+ ### Auto-connect
118
+
119
+ When enabled, the daemon keeps at least one shell alive and reconnects it on
120
+ failure, so attaching is instant.
121
+
122
+ ```bash
123
+ sshm e, enable <alias> # Keep a session alive, auto-reconnect
124
+ sshm d, disable <alias> # Stop auto-connect
125
+ ```
126
+
127
+ ### Import / export
128
+
129
+ Move hosts, including their keys, between machines as JSON.
130
+
131
+ ```bash
132
+ sshm export servers.json # Export all hosts
133
+ sshm export prod.json web db api # Export specific hosts
134
+ sshm l servers.json # Preview a JSON file
135
+ sshm import servers.json # Import (skip existing)
136
+ sshm import servers.json -o # Import (override existing)
137
+ sshm import servers.json web db # Import only specific hosts
138
+ sshm import servers.json web=prod # Import 'web' under the alias 'prod'
139
+ ```
140
+
141
+ ### Daemon
142
+
143
+ The daemon (`sshmd`) starts on first use.
144
+
145
+ ```bash
146
+ sshm status # Daemon status
147
+ sshm stop # Stop the daemon
148
+ sshm install # Autostart on login (systemd / launchd / Task Scheduler)
149
+ sshm uninstall # Remove autostart
150
+ ```
151
+
152
+ ## Shell completions
153
+
154
+ ### fish
155
+
156
+ `sshm` ships fish completions: subcommands, `port -L/-R/-D` flags, and host aliases
157
+ pulled live from `~/.ssh/config` (including the bare `sshm <alias>` shorthand).
158
+ `uv tool install` does **not** wire these up automatically, so install them once
159
+ (`exec fish` reloads the shell so they're active immediately):
160
+
161
+ ```fish
162
+ sshm completions fish > ~/.config/fish/completions/sshm.fish && exec fish
163
+ ```
164
+
165
+ fish autoloads from that directory — new sessions pick it up with no `source`. From
166
+ a checkout you can instead symlink the source file so edits are picked up live:
167
+
168
+ ```fish
169
+ ln -s (path resolve src/sshm/completions/sshm.fish) ~/.config/fish/completions/sshm.fish
170
+ ```
171
+
172
+ ## Session states
173
+
174
+ | Icon | State | Meaning |
175
+ |------|----------|-----------------------------------|
176
+ | `●` | ready | Shell running, waiting for attach |
177
+ | `◆` | attached | A client is connected |
178
+ | `○` | dead | Process exited |
179
+
180
+ ## How it works
181
+
182
+ ```
183
+ sshm (CLI) ── TCP localhost:19222 ──> sshmd (daemon)
184
+ ├── SSH processes (one shell per session)
185
+ ├── Reader threads (scrollback buffer)
186
+ ├── Watchdog (health, reconnect, keep-warm)
187
+ └── IPC server (JSON protocol + I/O bridge)
188
+ ```
189
+
190
+ - The daemon spawns `ssh -tt <alias>` under a real PTY (POSIX; pipes on Windows)
191
+ and holds the shell process.
192
+ - `connect` bridges your terminal I/O to that process over TCP and forwards your
193
+ terminal size (and resizes / `SIGWINCH`) so the remote shell matches your window.
194
+ - Detaching (closing the terminal, Ctrl-C) leaves the shell running; reattach later.
195
+ - Typing `exit` in the shell removes the session cleanly — no reconnect.
196
+ - A lost connection (SSH exit 255) triggers reconnect with exponential backoff.
197
+ - Config stays in `~/.ssh/config`. Every rewrite goes to a temp file and is
198
+ replaced atomically (a `config.bak` is kept), so a crash mid-write won't corrupt it.
199
+ - Runtime state lives in `~/.sshm/`: pid file, IPC token, `sshmd.log`.
200
+ - The IPC server binds `127.0.0.1` only and checks a random per-daemon token
201
+ (stored `0600` in `~/.sshm/token`) on every request.
202
+ - The IPC port defaults to `19222`; set `SSHM_PORT` to change it (e.g. to run sshm
203
+ in both Windows and WSL when mirrored networking shares localhost). `sshm install`
204
+ persists the port to `~/.sshm/port` so the autostarted daemon — which doesn't see
205
+ your shell env — uses the same one.
206
+
207
+ ## Platforms
208
+
209
+ - Linux — systemd user service for autostart.
210
+ - macOS — launchd LaunchAgent.
211
+ - Windows — Task Scheduler for autostart, Win32 console VT mode for terminal I/O.
212
+
213
+ On POSIX, ssh runs under a real PTY, so the remote shell tracks your window size
214
+ and resizes. Windows has no `pty` module, so there a session keeps the size it
215
+ attached with — full-screen apps (`vim`, `htop`) won't follow later resizes.
216
+
217
+ ## Development
218
+
219
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the full branch / commit / PR workflow.
220
+
221
+ ```bash
222
+ uv sync # install dependencies (including the dev group)
223
+ uv run pytest # run the tests
224
+ ```
225
+
226
+ Module layout (`src/sshm/`):
227
+
228
+ | Module | Responsibility |
229
+ |----------------|-----------------------------------------------------------|
230
+ | `cli.py` | CLI commands (click), entry point `sshm` |
231
+ | `daemon.py` | `sshmd`: request dispatch, watchdog, entry `sshmd` |
232
+ | `process.py` | SSH sessions: PTY spawn, scrollback, reconnect, health |
233
+ | `ipc.py` | TCP IPC client/server on `127.0.0.1:19222` (token auth) |
234
+ | `protocol.py` | JSON message schema for IPC |
235
+ | `config.py` | `~/.ssh/config` parser/writer, port-forward rules |
236
+ | `terminal.py` | Raw terminal bridge (termios on Unix, Win32 console API) |
237
+ | `procutil.py` | Cross-platform process helpers (pid checks, Popen flags) |
238
+ | `state.py` | `~/.sshm` runtime files: pid, token, port, log |
239
+ | `autostart.py` | Task Scheduler / systemd / launchd integration |
240
+
241
+ ## Troubleshooting
242
+
243
+ ### Windows: `Bad owner or permissions on ~/.ssh/config`
244
+
245
+ OpenSSH on Windows refuses to read `~/.ssh/config` if the ACL contains extra
246
+ principals like `OWNER RIGHTS` (S-1-3-4). Symptom:
247
+
248
+ ```
249
+ Bad permissions. Try removing permissions for user: \\OWNER RIGHTS (S-1-3-4) on file C:/Users/<you>/.ssh/config.
250
+ Bad owner or permissions on C:\\Users\\<you>/.ssh/config
251
+ ```
252
+
253
+ Remove the offending principal:
254
+
255
+ ```powershell
256
+ icacls "$env:USERPROFILE\.ssh\config" /remove "OWNER RIGHTS"
257
+ ```
258
+
259
+ If other files in `.ssh` have the same issue (private keys, etc.):
260
+
261
+ ```powershell
262
+ icacls "$env:USERPROFILE\.ssh\*" /remove "OWNER RIGHTS"
263
+ ```
264
+
265
+ If that's not enough (inheritance can pull in other groups), reset the ACL so only
266
+ you have access:
267
+
268
+ ```powershell
269
+ $f = "$env:USERPROFILE\.ssh\config"
270
+ icacls $f /inheritance:r
271
+ icacls $f /grant:r "$($env:USERNAME):(F)"
272
+ ```
273
+
274
+ If `icacls` returns `Access is denied`, you don't own the file. Take ownership
275
+ first, then fix the ACL:
276
+
277
+ ```powershell
278
+ takeown /F "$env:USERPROFILE\.ssh\config"
279
+ icacls "$env:USERPROFILE\.ssh\config" /grant "$($env:USERNAME):F"
280
+ icacls "$env:USERPROFILE\.ssh\config" /inheritance:r
281
+ icacls "$env:USERPROFILE\.ssh\config" /grant:r "$($env:USERNAME):(F)"
282
+ ```
283
+
284
+ If `takeown` fails, run PowerShell as Administrator and repeat. Verify with
285
+ `icacls "$env:USERPROFILE\.ssh\config"` — only your user with `(F)` should remain.
286
+
287
+ ## License
288
+
289
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,45 @@
1
+ [project]
2
+ name = "sshmd"
3
+ version = "0.1.0"
4
+ description = "Cross-platform SSH session manager with a background daemon"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ keywords = ["ssh", "session-manager", "daemon", "port-forwarding", "socks", "terminal"]
10
+ classifiers = [
11
+ "Environment :: Console",
12
+ "Intended Audience :: System Administrators",
13
+ "Operating System :: OS Independent",
14
+ "Programming Language :: Python :: 3",
15
+ "Topic :: System :: Networking",
16
+ "Topic :: Utilities",
17
+ ]
18
+ dependencies = [
19
+ "click>=8.0",
20
+ ]
21
+
22
+ [project.urls]
23
+ Homepage = "https://github.com/revsearch/sshm"
24
+ Repository = "https://github.com/revsearch/sshm"
25
+ Issues = "https://github.com/revsearch/sshm/issues"
26
+
27
+ [project.scripts]
28
+ sshm = "sshm.cli:main"
29
+ sshmd = "sshm.daemon:main"
30
+
31
+ [build-system]
32
+ requires = ["uv_build>=0.10.6,<0.12.0"]
33
+ build-backend = "uv_build"
34
+
35
+ # The distribution is published as `sshmd`, but the import package is `sshm`.
36
+ [tool.uv.build-backend]
37
+ module-name = "sshm"
38
+
39
+ [dependency-groups]
40
+ dev = [
41
+ "pytest>=8.0",
42
+ ]
43
+
44
+ [tool.pytest.ini_options]
45
+ testpaths = ["tests"]
@@ -0,0 +1,3 @@
1
+ """sshm — cross-platform SSH session manager with a background daemon."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ """Allow running as python -m sshm."""
2
+ from .cli import main
3
+
4
+ main()