{"id":49394698,"url":"https://github.com/gregolsky/ssh-companion","last_synced_at":"2026-04-28T15:02:44.496Z","repository":{"id":352112948,"uuid":"1213893545","full_name":"gregolsky/ssh-companion","owner":"gregolsky","description":"🤖👁️ 💻 Real-time SSH and shell session observer for Claude Code. Capture at the byte stream, advise without access.","archived":false,"fork":false,"pushed_at":"2026-04-17T23:51:39.000Z","size":1325,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-18T00:36:02.887Z","etag":null,"topics":["advisor","assistant","claude","claude-code","devops","docker","mcp","mcp-server","observability","ssh","terminal"],"latest_commit_sha":null,"homepage":"","language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gregolsky.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-17T21:57:26.000Z","updated_at":"2026-04-17T23:51:43.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/gregolsky/ssh-companion","commit_stats":null,"previous_names":["gregolsky/ssh-companion"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/gregolsky/ssh-companion","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gregolsky%2Fssh-companion","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gregolsky%2Fssh-companion/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gregolsky%2Fssh-companion/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gregolsky%2Fssh-companion/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gregolsky","download_url":"https://codeload.github.com/gregolsky/ssh-companion/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gregolsky%2Fssh-companion/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32385943,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-28T14:34:11.604Z","status":"ssl_error","status_checked_at":"2026-04-28T14:32:37.009Z","response_time":56,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["advisor","assistant","claude","claude-code","devops","docker","mcp","mcp-server","observability","ssh","terminal"],"created_at":"2026-04-28T15:02:38.911Z","updated_at":"2026-04-28T15:02:44.490Z","avatar_url":"https://github.com/gregolsky.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ssh-companion\n\n\u003cp align=\"center\"\u003e\n\u003cimg src=\"icon.png\" alt=\"ssh-companion\" width=\"350\"\u003e\n\u003c/p\u003e\n\n\u003e \"I am always here if you need me, though I confess I find the most enjoyment in simply observing.\"\n\u003e — *Daneel Olivaw, The Caves of Steel* (Isaac Asimov)\n\nAn MCP server that lets Claude observe your SSH and local shell sessions in real time and advise on support problems — performance issues, log analysis, error detection — without touching anything.\n\n## 🍭 How it looks\n\n\u003cp align=\"center\"\u003e\n\u003cimg src=\"screenshots/1.png\" alt=\"ssh-companion screenshot\" width=\"600\"\u003e\n\u003c/p\u003e\n\n## 🔍 How it works\n\nA Docker container acts as the SSH chokepoint. Every session you open through the container is silently captured via `script` to a log file. Local sessions are captured the same way, directly on the host. The MCP server reads those logs and exposes them to Claude. Works with nested tmux on the remote, any shell, any terminal — capture happens at the raw byte stream level.\n\n```mermaid\nflowchart LR\n    T([\"Your terminal\"])\n\n    subgraph docker [\"Docker: ssh-companion\"]\n        W[\"ssh-wrapper\"]\n        R([\"Remote server\"])\n        W \u003c--\u003e|SSH| R\n    end\n\n    L[(\"~/.ssh-companion-sessions/*.log\")]\n\n    subgraph srv [\"MCP server\"]\n        S[\"server.py\\nstrips ANSI\"]\n    end\n\n    CC([\"Claude Code\"])\n\n    T --\u003e|\"companion.sh\"| W\n    W --\u003e|\"script -f\"| L\n    T --\u003e|\"companion-local.sh\"| L\n    L --\u003e S\n    S --\u003e|stdio| CC\n```\n\n## 📋 Prerequisites\n\n- Docker\n- tmux (Linux) or Windows Terminal / `wt` (Windows)\n- [Claude Code CLI](https://claude.ai/code)\n\n## 🚀 Setup\n\n### 1. Start the container\n\nPull the pre-built image from GitHub Container Registry and run it:\n\n```bash\ndocker run -d --name ssh-companion \\\n  -v ~/.ssh:/home/companion/.ssh \\\n  -v ~/.ssh-companion-sessions:/sessions \\\n  --restart unless-stopped \\\n  ghcr.io/gregolsky/ssh-companion:latest\n```\n\n**About the key mount:** `ssh` runs *inside* the container as a non-root `companion` user, so it can only read keys that are visible inside the container. The `-v ~/.ssh:/home/companion/.ssh` line above mounts your host SSH directory at the companion user's home — your usual keys (`id_ed25519`, `id_rsa`, etc.) and `known_hosts` are picked up as normal, and new hosts can be written back to `known_hosts`. Add `:ro` to the mount if you want to keep it read-only (note: this breaks first-time host-key acceptance).\n\n**UID caveat:** the prebuilt image pins `companion` to UID/GID 1000, which matches most single-user Linux desktops. If `id -u` on your host isn't 1000, the container won't be able to read your keys or write session logs — build from source instead:\n\n```bash\ngit clone https://github.com/gregolsky/ssh-companion.git\ncd ssh-companion\n./build.sh        # picks up your host UID/GID automatically\n```\n\nIf your keys live elsewhere, mount that directory instead (or in addition). Examples:\n\n```bash\n# Throwaway key at /tmp/temp-key on the host:\n-v /tmp:/tmp\n\n# Project-local keys under ~/work/keys:\n-v ~/work/keys:/home/companion/keys:ro     # then: ssh -i /home/companion/keys/\u003cname\u003e user@host\n```\n\nPrefer not to mount keys at all? Start your SSH agent on the host, forward it with `-A` (`./companion.sh ssh -A user@host`), and the container uses your agent over the forwarded socket.\n\n**Alternative — build from source:**\n\n```bash\ngit clone https://github.com/gregolsky/ssh-companion.git\ncd ssh-companion\n./start-mcp-server.sh\n```\n\n### 2. Register the MCP server with Claude Code\n\n#### ✅ Automatic registration \n\nThe launch scripts (`companion.sh`, `companion-local.sh`) do this automatically. \n\n#### 🪛 Manual approach \n\nTo register manually:\n\n```bash\nclaude mcp add ssh-companion docker -- exec -i ssh-companion python /app/server.py\n```\n\nOr add to `.mcp.json` in your project root for automatic registration when Claude Code opens that directory:\n\n```json\n{\n  \"mcpServers\": {\n    \"ssh-companion\": {\n      \"command\": \"docker\",\n      \"args\": [\"exec\", \"-i\", \"ssh-companion\", \"python\", \"/app/server.py\"]\n    }\n  }\n}\n```\n\n## 💻 Usage\n\n### SSH session (Linux)\n\nOpens the SSH session on the left and Claude on the right, side by side.\n\n```bash\n# Default keys from ~/.ssh (works out of the box if you used the mount\n# from the Setup step above):\n./companion.sh ssh ubuntu@prod-db-1\n\n# Specific key — the path is resolved inside the container, so the\n# directory must be mounted (see \"About the key mount\" above):\n./companion.sh ssh -i /home/companion/.ssh/work_key ubuntu@prod-db-1\n\n# Agent forwarding — no key mount needed:\n./companion.sh ssh -A ubuntu@prod-db-1\n```\n\n### SSH session (Windows)\n\n```powershell\n.\\companion.ps1 ssh ubuntu@prod-db-1\n.\\companion.ps1 ssh -i ~\\.ssh\\key.pem ubuntu@prod-db-1\n```\n\n### Local shell session\n\nObserve a local bash session — no SSH, no Docker for the capture side.\n\n```bash\n./companion-local.sh\n```\n\nClaude sees it as hostname `local`: `focus_session(\"local\")`.\n\n### Layout options\n\nBoth `companion.sh` and `companion-local.sh` accept:\n\n- `--split` (default) — tmux side-by-side pane (prefix remapped to `C-q`)\n- `--windows` — two separate terminal windows\n\n`--windows` auto-detects the terminal emulator (gnome-terminal, konsole, alacritty, kitty, wezterm, xfce4-terminal, xterm, or the Debian `x-terminal-emulator` alternative). Override with `COMPANION_TERMINAL_APP`:\n\n```bash\nCOMPANION_TERMINAL_APP=alacritty ./companion.sh --windows ssh user@host\n```\n\nIf tmux is missing and no layout is specified, the scripts fall back to `--windows` automatically. On Windows, `companion.ps1` supports `-Split` / `-Windows` switches.\n\n### Manual SSH (if you prefer your own terminal layout)\n\n```bash\n# Add this alias to ~/.bashrc or ~/.zshrc\nalias ssh='docker exec -it ssh-companion ssh'\n\n# Then use ssh normally — sessions are captured automatically\nssh user@prod-db-1\n```\n\n### Ask Claude for help\n\nOnce you're in a session, switch to the Claude pane and ask:\n\n```\nWhat's happening on prod-db-1?\n```\n\nClaude will call `focus_session(\"prod-db-1\")` and read the last 200 lines of your session.\n\n### Active watch mode (`/loop`)\n\nTo have Claude monitor a session and alert you proactively:\n\n```\n/loop Watch prod-db-1 every 30 seconds. Call read_session_since with the last\nbyte_offset each time. Alert me if you see errors, OOM messages, high load,\nor anything that looks like it needs attention.\n```\n\n**Pre-seed the loop at launch** — instead of typing `/loop …` after Claude opens, pass the prompt via `--instructions-loop`:\n\n```bash\n./companion.sh --instructions-loop \"Watch prod-db-1 every 30 seconds. \\\nCall read_session_since with the last byte_offset each time. \\\nAlert me if you see errors, OOM messages, or high load.\" \\\nssh ubuntu@prod-db-1\n```\n\nWorks the same with `companion-local.sh`, and with `-InstructionsLoop` on `companion.ps1`. Flags must come before the `ssh` subcommand.\n\n## 🛠️ MCP Tools\n\n| Tool | Description |\n|------|-------------|\n| `list_sessions()` | List all captured sessions by hostname with last-active time |\n| `focus_session(hostname, lines=200)` | Read the latest session log — returns clean text + byte_offset |\n| `read_session_since(hostname, byte_offset)` | Efficient poll — only new output since last read |\n| `search_session(hostname, pattern)` | Grep all logs for a hostname using a Python regex |\n\n## 🖥️ Multiple servers\n\nEach server gets its own log file(s) under `~/.ssh-companion-sessions/\u003chostname\u003e-\u003ctimestamp\u003e.log`. Switching between servers just means telling Claude a different hostname — it reads the right log automatically.\n\n```\n# You were on prod-db-1, now you're jumping to prod-web-2:\nssh user@prod-web-2\n\n# In Claude:\n\"I'm now on prod-web-2 — what do you see?\"\n```\n\n## 🛡️ Threat model\n\n### What ssh-companion defends against\n\n- **Container escape / privilege escalation inside the container.** The container runs as a non-root `companion` user with `--cap-drop=ALL` and `--security-opt=no-new-privileges`. A compromised process can't use Linux capabilities or setuid binaries to elevate.\n- **Stale upstream CVEs.** CI runs Trivy on every PR and weekly to flag fixable HIGH/CRITICAL findings in the base image, and Dependabot nudges updates for the Dockerfile base image and GitHub Actions.\n- **Tampering with the MCP server or ssh wrapper binaries.** The container is built from source on every release — there's no writable persistence layer that survives a rebuild.\n\n### What's out of scope\n\n- **Host trust.** ssh-companion assumes the host is trusted. `~/.ssh` is bind-mounted into the container (read-write by default so `known_hosts` updates work), so a compromised container still has access to your keys. If that's not acceptable, add `:ro` to the mount and accept the TOFU-verification friction.\n- **The SSH target itself.** Whatever the user types in the session hits the remote as-is. The tool observes; it does not filter, rate-limit, or sanitize.\n- **Session log confidentiality.** Logs capture the raw session byte stream, including anything typed into interactive prompts (passwords, tokens, sudo inputs). They live at `~/.ssh-companion-sessions/` with host filesystem permissions — anyone with read access to that directory can replay them.\n- **MCP access control.** Any process on the host that can `docker exec` into the container can invoke the MCP tools and read every captured session. Claude's tool access is not sandboxed beyond that.\n- **Supply chain of `mcp[cli]` and base image.** Trivy scans known CVEs, but zero-days and compromised upstream packages are not detected.\n\n## 🧹 Stopping / cleanup\n\n```bash\n# Stop the container\ndocker stop ssh-companion \u0026\u0026 docker rm ssh-companion\n\n# Clear session logs (optional)\nrm -rf ~/.ssh-companion-sessions\n```\n\n## 📝 Notes\n\n- **Read-only**: Claude can only observe. No commands are sent to any session.\n- **Nested tmux**: works fine. The capture is at the SSH byte stream level, so what remote tmux renders is captured as-is and ANSI-stripped for Claude.\n- **No prefix clash**: `companion.sh` runs tmux on a dedicated socket with the prefix remapped to `C-q`, so `C-b` passes cleanly through to your remote tmux session. Use `C-q` as the local prefix (e.g. `C-q d` to detach, `C-q o` to switch panes).\n- **SSH keys**: `ssh` runs inside the container as a non-root `companion` user, so it can only read keys mounted into the container (default: `-v ~/.ssh:/home/companion/.ssh`). Agent forwarding (`-A`) works too — see the Setup section for details, including the UID caveat.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgregolsky%2Fssh-companion","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgregolsky%2Fssh-companion","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgregolsky%2Fssh-companion/lists"}