Recently Written · git

sshp-ai

Agent skill + read-only MCP server for parallel SSH fleet checks with bahamas10/sshp

git clone https://github.com/equwal/sshp-ai

Log | Files | Refs


commit eab3b42d5b593b62d435de5e72512485fe2f6e79
equwal <13551856+equwal@users.noreply.github.com>
2026-10-03 21:44:10 -0700

Write the sshp hosts file with LF line endings

On Windows the temp hosts file got CRLF, and sshp rejected each host name with a trailing CR. The new test checks the bytes sshp receives.

 sshp_mcp.py      |  3 ++-
 test_sshp_mcp.py | 21 +++++++++++++++++++++
 2 files changed, 23 insertions(+), 1 deletion(-)
diff --git a/sshp_mcp.py b/sshp_mcp.py
index 1d9e2a9..084124d 100644
--- a/sshp_mcp.py
+++ b/sshp_mcp.py
@@ -122,7 +122,8 @@ def sshp_run(args, cfg):
     for opt in cfg["ssh_options"]:
         argv += ["-o", opt]
 
-    with tempfile.NamedTemporaryFile("w", suffix=".hosts", delete=False) as f:
+    # newline="\n": sshp keeps a CR from a Windows CRLF line as part of the host name.
+    with tempfile.NamedTemporaryFile("w", suffix=".hosts", delete=False, newline="\n") as f:
         f.write("\n".join(hosts) + "\n")
         hosts_path = f.name
     try:
diff --git a/test_sshp_mcp.py b/test_sshp_mcp.py
index 5bfe9b1..8aaf793 100644
--- a/test_sshp_mcp.py
+++ b/test_sshp_mcp.py
@@ -66,6 +66,27 @@ class PolicyTest(unittest.TestCase):
         finally:
             os.unlink(f.name)
 
+    def test_hosts_file_has_no_cr(self):
+        # Regression: on Windows a CRLF temp file made sshp reject "host\r".
+        seen = {}
+
+        def fake_run(argv, **kw):
+            with open(argv[argv.index("-f") + 1], "rb") as f:
+                seen["data"] = f.read()
+            return subprocess.CompletedProcess(argv, 0, "[a.example.com] exited: 0 (1 ms)\n", "")
+
+        with tempfile.NamedTemporaryFile("w", delete=False) as f:
+            f.write("a.example.com\n")
+        old = sshp_mcp.subprocess.run
+        sshp_mcp.subprocess.run = fake_run
+        try:
+            out = sshp_mcp.sshp_run({"command": "uptime"}, cfg(f.name))
+        finally:
+            sshp_mcp.subprocess.run = old
+            os.unlink(f.name)
+        self.assertEqual(seen["data"], b"a.example.com\n")
+        self.assertEqual(out["hosts"]["a.example.com"]["exit_code"], 0)
+
     def test_bad_user(self):
         with tempfile.NamedTemporaryFile("w", delete=False) as f:
             f.write("a.example.com\n")