
Ssh Skill
- 92 installs
- 458 repo stars
- Updated May 23, 2026
- badseal/ssh-skill
ssh-skill is a Claude skill that routes all SSH and SCP operations through helper scripts to provide persistent connection pooling, jump hosts, SFTP, and tunneling.
About
ssh-skill is a skill that wraps all SSH and remote-server operations through Python scripts instead of raw ssh/scp. A developer uses it to execute remote commands, upload and download files, transfer between servers, open tunnels, and run commands across a server cluster. It maintains a background daemon for persistent connection pooling and automatic reconnection.
- Runs remote commands and file transfers over pooled, persistent SSH connections
- Supports jump hosts (ProxyJump), SFTP, tunneling, and server-to-server transfer
- Daemon mode cuts single-command latency from ~0.45s to ~0.12s
Ssh Skill by the numbers
- 92 all-time installs (skills.sh)
- Ranked #582 of 1,039 Cloud & Infrastructure skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
ssh-skill capabilities & compatibility
- Capabilities
- remote execution · file transfer · ssh tunnel · cluster management
- Use cases
- devops
- Platforms
- Windows · Linux · macOS
- Runs
- Runs locally
- Pricing
- Free
What ssh-skill says it does
Provides persistent connections, pooling, jump hosts, SFTP, tunneling, and recovery.
守护进程长连接:首次连接后自动启动守护进程,后续命令响应时间从 ~0.45s 降至 ~0.12s
DO NOT use for local commands, localhost, or current-directory work.
npx skills add https://github.com/badseal/ssh-skill --skill ssh-skillAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 92 |
|---|---|
| repo stars | ★ 458 |
| Last updated | May 23, 2026 |
| Repository | badseal/ssh-skill ↗ |
What it does
Execute commands, transfer files, and open tunnels across remote servers through pooled SSH connections and jump hosts.
Who is it for?
Managing many remote servers with persistent SSH connections, jump hosts, and batch command execution.
Skip if: Local commands, localhost, or current-directory work (the docs say DO NOT use for these).
When should I use this skill?
Any SSH or remote-server operation: connect, run a command on a server, transfer files, open a tunnel, or run across a cluster.
What you get
Remote commands and transfers run over pooled connections with lower latency and automatic reconnection.
- Remote command output
- Uploaded/downloaded files
- Active SSH tunnels
By the numbers
- Daemon mode ~0.12s per command vs ~0.45s direct
- Supports 4 transfer modes: direct, stream, hybrid, auto
- Auto-reconnect retries up to 3 times
Files
SSH Skill v3.3.1
高性能 SSH 操作技能,支持守护进程长连接、自动连接复用、跳板机、批量并发、服务器间直接传输、自动错误恢复。
快捷命令
当用户通过 /ssh-skill <参数> 调用本 skill 时,根据参数执行对应操作:
/ssh-skill list
列出所有已配置的服务器。执行以下步骤:
1. 运行命令获取数据:
python ~/.claude/skills/ssh-skill/scripts/ssh_config_manager_v3.py list-servers2. 解析返回的 JSON 数据 3. 以 Markdown 表格 格式展示,列:序号、别名、备注(description)、标签(tags)、位置(location)、认证方式(auth)、用户名(user) 4. 在表格末尾显示服务器总数
表格示例格式:
| # | 别名 | 备注 | 标签 | 位置 | 认证 | 用户名 |
|---|------|------|------|------|------|--------|
| 1 | mgmt-01 | 管理服务器 | 管理,Warpgate | 丰台机房 | 密钥 | root |/ssh-skill find <关键词>
查找匹配的服务器,格式同 list。
/ssh-skill help
展示 SSH Skill 的帮助文档。以 Markdown 格式输出以下内容:
SSH Skill v3.3.1 - 高性能 SSH 操作技能
核心特点:
- 守护进程长连接:首次连接后自动启动守护进程,后续命令响应时间从 ~0.45s 降至 ~0.12s
- 自动连接复用:多个 Claude Code 实例可共享同一守护进程
- SFTP 高级传输:支持断点续传、进度显示、目录递归上传/下载
- 服务器间直接传输:支持服务器到服务器的文件直接传输,无需本地中转
- SSH 隧道:支持本地端口转发,访问远程内网服务(数据库、Web 服务等)
- 跳板机支持:通过 ProxyJump 自动处理多级跳板机
- 批量并发操作:支持对多台服务器并发执行命令
- 自动错误恢复:SSH 连接断开自动重连(最多 3 次)
快捷命令:
/ssh-skill list- 列出所有已配置的服务器/ssh-skill find <关键词>- 查找匹配的服务器/ssh-skill transfer <源> <源路径> <目标> <目标路径>- 服务器间文件传输/ssh-skill tunnel <别名> <端口>- 启动 SSH 隧道/ssh-skill help- 显示此帮助信息
常用操作:
1. 执行远程命令:
在 <别名> 上执行 <命令>2. 上传文件:
上传 <本地路径> 到 <别名> 的 <远程路径>3. 下载文件:
从 <别名> 下载 <远程路径> 到 <本地路径>4. 服务器间传输:
从 <源别名> 传输 <路径> 到 <目标别名> 的 <路径>
将 <别名A> 的文件迁移到 <别名B>5. SSH 隧道:
建立到 <别名> 的 MySQL 隧道
连接 <别名> 的数据库
访问 <别名> 的内部服务6. 批量操作:
在所有服务器上执行 <命令>
在生产环境服务器上执行 <命令>配置管理:
- 配置文件位置:
~/.ssh/config - 使用标准 OpenSSH 格式 + 注释元数据
- 支持密钥认证和密码认证
- 支持 ProxyJump 跳板机配置
性能对比:
- 直连模式:单次命令 ~0.45s,连续 10 条 ~4.5s
- 守护进程模式:单次命令 ~0.12s,连续 10 条 ~1.2s
更多详细信息请参考 SKILL.md 文档。
其他参数
将参数作为用户意图理解,按照下方调用规则执行对应的 SSH 操作。
CRITICAL: 调用规则
路径说明
默认路径:~/.claude/skills/ssh-skill/scripts
~会自动展开为用户家目录(Windows 和 Linux 通用)- Windows:
C:\Users\用户名\.claude\skills\ssh-skill\scripts - Linux:
/home/用户名/.claude/skills/ssh-skill/scripts
项目目录中的 skill:如果 skill 放在项目的 .claude/skills/ssh-skill/ 中,使用相对路径:
.claude/skills/ssh-skill/scripts路径自动识别:Python 的 os.path.expanduser() 会自动处理 ~,无需手动替换。
调用格式(唯一正确方式)
MUST: 使用 python ~/.claude/skills/ssh-skill/scripts/脚本名.py 格式。使用别名(alias)标识服务器。
NEVER: 不要使用 cd 到脚本目录再执行,不要使用反斜杠 \,不要直接写 ssh 或 scp 命令。
执行远程命令
python ~/.claude/skills/ssh-skill/scripts/ssh_execute.py <别名> "<命令>"可选参数:--timeout <秒> --no-daemon
ssh_execute.py 会自动检测守护进程:有则走长连接(~0.12s),无则自动启动守护进程。
上传文件
MSYS_NO_PATHCONV=1 python ~/.claude/skills/ssh-skill/scripts/ssh_upload.py <别名> "<本地路径>" "<远程路径>"可选参数:--resume(断点续传) --recursive(目录递归上传) --no-progress(禁用进度输出)
下载文件
MSYS_NO_PATHCONV=1 python ~/.claude/skills/ssh-skill/scripts/ssh_download.py <别名> "<远程路径>" "<本地路径>"可选参数:--resume(断点续传) --recursive(目录递归下载) --no-progress(禁用进度输出)
CRITICAL: 上传/下载命令必须加 MSYS_NO_PATHCONV=1 前缀,防止 Windows MSYS bash 将远程路径(如 /tmp/file)转换为 Windows 路径。
服务器间传输
# 自动模式(推荐)- 根据文件大小和网络环境自动选择最优方式
MSYS_NO_PATHCONV=1 python "~/.claude/skills/ssh-skill/scripts/ssh_server_transfer.py" <源别名> "<源路径>" <目标别名> "<目标路径>"
# 强制直连模式(大文件推荐,数据直接在服务器间传输)
MSYS_NO_PATHCONV=1 python "~/.claude/skills/ssh-skill/scripts/ssh_server_transfer.py" <源别名> "<源路径>" <目标别名> "<目标路径>" --mode direct
# 强制流式转发(小文件或服务器间网络不通时)
MSYS_NO_PATHCONV=1 python "~/.claude/skills/ssh-skill/scripts/ssh_server_transfer.py" <源别名> "<源路径>" <目标别名> "<目标路径>" --mode stream
# 混合模式(先尝试直连,失败后自动降级到流式)
MSYS_NO_PATHCONV=1 python "~/.claude/skills/ssh-skill/scripts/ssh_server_transfer.py" <源别名> "<源路径>" <目标别名> "<目标路径>" --mode hybrid
# 使用 rsync(仅直连模式,支持增量同步)
MSYS_NO_PATHCONV=1 python "~/.claude/skills/ssh-skill/scripts/ssh_server_transfer.py" <源别名> "<源路径>" <目标别名> "<目标路径>" --use-rsync可选参数:--mode <auto|direct|stream|hybrid>(传输模式) --use-rsync(使用 rsync) --no-progress(禁用进度) --size-threshold <MB>(大小阈值,默认 10) --timeout <秒>(超时,默认 300)
传输模式说明:
| 模式 | 适用场景 | 数据流向 | 优点 |
|---|---|---|---|
| 直连 (direct) | 大文件、服务器间网络通 | 源服务器 → 目标服务器 | 速度快,不占本地带宽 |
| 流式 (stream) | 小文件、网络不通 | 源 → 本地 → 目标(流式) | 无需服务器间配置 |
| 混合 (hybrid) | 不确定环境 | 先尝试直连,失败降级 | 自动适应 |
| 自动 (auto) | 默认 | 智能判断 | 最优选择 |
CRITICAL: 服务器间传输命令也必须加 MSYS_NO_PATHCONV=1 前缀。
批量操作
# 对所有服务器执行
python "~/.claude/skills/ssh-skill/scripts/ssh_cluster.py" "<命令>" --parallel
# 对指定别名列表执行
python "~/.claude/skills/ssh-skill/scripts/ssh_cluster.py" "<命令>" --hosts "DEV-002,DEV-003" --parallel
# 按环境过滤
python "~/.claude/skills/ssh-skill/scripts/ssh_cluster.py" "<命令>" --environment production --parallel
# 按标签过滤
python "~/.claude/skills/ssh-skill/scripts/ssh_cluster.py" "<命令>" --tags "web,nginx" --parallel可选参数:--timeout <秒> --health-check --max-workers <数量>
配置管理
# 列出所有服务器
python "~/.claude/skills/ssh-skill/scripts/ssh_config_manager_v3.py" list-servers
# 按环境过滤
python "~/.claude/skills/ssh-skill/scripts/ssh_config_manager_v3.py" list-servers --environment production
# 查找服务器(支持别名和描述模糊查找)
python "~/.claude/skills/ssh-skill/scripts/ssh_config_manager_v3.py" find "<关键词>"
# 创建配置
python "~/.claude/skills/ssh-skill/scripts/ssh_config_manager_v3.py" create --alias <别名> --host <IP> --user <用户名> --key <密钥文件> --environment <环境>
# 更新配置(只更新提供的字段,其他字段保持不变)
python "~/.claude/skills/ssh-skill/scripts/ssh_config_manager_v3.py" update <别名> --description "新描述" --tags tag1 tag2 tag3
python "~/.claude/skills/ssh-skill/scripts/ssh_config_manager_v3.py" update <别名> --environment production --location "新位置"
python "~/.claude/skills/ssh-skill/scripts/ssh_config_manager_v3.py" update <别名> --host <新IP> --port <新端口>
# 删除配置
python "~/.claude/skills/ssh-skill/scripts/ssh_config_manager_v3.py" delete <别名>SSH Tunnel(端口转发)
本地端口转发 - 通过 SSH 隧道访问远程服务
# 启动 tunnel(自动分配本地端口)
python "~/.claude/skills/ssh-skill/scripts/ssh_tunnel.py" start <别名> --remote-port <端口>
# 指定本地端口
python "~/.claude/skills/ssh-skill/scripts/ssh_tunnel.py" start <别名> --local-port <本地端口> --remote-port <远程端口>
# 转发到远程的其他主机
python "~/.claude/skills/ssh-skill/scripts/ssh_tunnel.py" start <别名> --remote-host <远程主机> --remote-port <端口>
# 列出所有活动的 tunnel
python "~/.claude/skills/ssh-skill/scripts/ssh_tunnel.py" list
# 查看 tunnel 状态
python "~/.claude/skills/ssh-skill/scripts/ssh_tunnel.py" status <tunnel-id>
# 停止 tunnel
python "~/.claude/skills/ssh-skill/scripts/ssh_tunnel.py" stop <tunnel-id>
# 停止服务器的所有 tunnel
python "~/.claude/skills/ssh-skill/scripts/ssh_tunnel.py" stop-all <别名>使用场景:
- 连接远程数据库(MySQL、PostgreSQL、Redis)
- 访问内部 Web 服务(管理后台、监控面板)
- 调试内部 API 接口
- 访问其他内部服务(Elasticsearch、RabbitMQ)
特性:
- 守护进程模式运行
- 自动重连和心跳检测
- 空闲 30 分钟自动退出
- 支持跳板机(ProxyJump)
- 只监听 localhost(安全)
示例:
# 连接远程 MySQL
python ssh_tunnel.py start prod-db-01 --remote-port 3306
# 返回:本地端口 10001
# 使用 tunnel 连接数据库
mysql -h 127.0.0.1 -P 10001 -u root -p
# 访问内部 Web 服务
python ssh_tunnel.py start prod-web-01 --remote-port 8080
# 然后在浏览器访问 http://127.0.0.1:10002配置文件
存储位置
~/.ssh/config(标准 OpenSSH 配置文件)
配置格式
每个服务器由 Host 块和注释元数据组成:
# ===== prod-web-01 =====
# description: 生产环境 Web 服务器
# environment: production
# tags: web,nginx,production
# location: 阿里云-北京
# password:
# created_at: 2026-03-01 12:00:00
# updated_at: 2026-03-01 12:00:00
Host prod-web-01
HostName 192.168.1.100
User root
IdentityFile ~/.ssh/id_rsa
Port 22密码认证配置
密码存储在注释中(SSH config 不原生支持密码字段):
# ===== dev-server =====
# description: 开发服务器
# environment: development
# password: your-password
Host dev-server
HostName 192.168.1.200
User root
Port 22注意:密码认证性能较低,建议升级为密钥认证。
跳板机配置
使用标准 ProxyJump:
Host bastion
HostName bastion.example.com
User jumpuser
IdentityFile ~/.ssh/jump_key
Host internal-server
HostName 10.0.1.100
User appuser
IdentityFile ~/.ssh/id_rsa
ProxyJump bastionAI 只需要知道 internal-server 这个别名,底层自动处理跳转。
守护进程(长连接模式)
工作原理
守护进程在本地维护到远程服务器的 Paramiko 长连接,通过本地 TCP 接受命令请求。 ssh_execute.py 自动检测守护进程:有则复用长连接,无则自动启动。
自动模式(推荐)
ssh_execute.py 首次调用时会自动启动守护进程,无需手动操作。 守护进程空闲 30 分钟后自动退出。
手动管理
# 启动守护进程(通常不需要手动启动)
python "~/.claude/skills/ssh-skill/scripts/ssh_daemon.py" start <别名>
# 查看守护进程状态
python "~/.claude/skills/ssh-skill/scripts/ssh_daemon.py" status <别名>
# 停止守护进程
python "~/.claude/skills/ssh-skill/scripts/ssh_daemon.py" stop <别名>可选参数:--idle-timeout <秒>(默认 1800,即 30 分钟)
守护进程特性
- 每台服务器独立守护进程,按别名隔离
- 多个对话(多个 Claude Code 实例)可共享同一守护进程
- SSH 连接断开自动重连(最多 3 次)
- 每 60 秒心跳检测连接状态
- 空闲超时自动退出,无需手动清理
性能对比
| 模式 | 单次命令 | 连续 10 条 | 连续 30 条 |
|---|---|---|---|
| 直连 | ~0.45s | ~4.5s | ~13.5s |
| 守护进程 | ~0.12s | ~1.2s | ~3.6s |
性能优化建议
命令合并
对同一服务器的多个独立查询,优先合并为一次调用:
# 好:一次调用获取多个信息
python "SCRIPTS/ssh_execute.py" DEV-002 "hostname && uptime && df -h && free -m"
# 差:多次调用分别获取
python "SCRIPTS/ssh_execute.py" DEV-002 "hostname"
python "SCRIPTS/ssh_execute.py" DEV-002 "uptime"
python "SCRIPTS/ssh_execute.py" DEV-002 "df -h"
python "SCRIPTS/ssh_execute.py" DEV-002 "free -m"何时合并,何时分开
- 合并:多个只读查询、状态检查、信息收集
- 分开:命令之间有依赖关系、需要根据前一个结果决定下一步、需要独立的错误处理
输出格式
所有脚本输出 JSON 格式:
{
"success": true,
"exit_code": 0,
"stdout": "命令输出",
"stderr": ""
}故障排查
连接超时
检查:网络连接、服务器是否在线、防火墙规则、跳板机是否可达。
长命令可加 --timeout 300 延长超时。
守护进程问题
如果守护进程异常,可手动停止后重试:
python "~/.claude/skills/ssh-skill/scripts/ssh_daemon.py" stop <别名>或使用 --no-daemon 参数跳过守护进程直连:
python "~/.claude/skills/ssh-skill/scripts/ssh_execute.py" <别名> "<命令>" --no-daemon别名不存在
如果提示别名不存在,可通过配置管理工具查找:
python "~/.claude/skills/ssh-skill/scripts/ssh_config_manager_v3.py" find "<关键词>"强制规则
- 所有 SSH 操作必须通过本 skill 的 Python 脚本
- 禁止直接写
ssh或scp命令(首次配置公钥除外) - 路径必须使用正斜杠
/,不要使用反斜杠\ - 不要用
cd切换到脚本目录,直接用完整路径调用 - 使用别名(alias)标识服务器,不再使用 JSON 配置文件路径
- 对同一服务器的多个只读查询,优先合并为一次调用
依赖
- Python 3.8+
- paramiko(SSH 连接和文件传输)
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
/lib/
/lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual Environment
venv/
ENV/
env/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Logs
*.log
# Temporary files
*.tmp
*.bak
*.orig
# SSH keys (安全起见)
*.pem
*.key
id_rsa*
id_ed25519*
# Local configuration
config.local.json
.env
# Docs (本地文档,不提交)
docs/
"""
SSH Skill 基本用法(可运行)
说明:
- 这是一个“回归用例”脚本:用本目录的示例配置调用 ssh-skill 的 CLI 入口。
- 运行前请先把示例配置中的占位符替换为你的真实信息(不要提交到仓库)。
用法:
python basic_usage.py [config_json] [command]
示例:
python basic_usage.py ./config_single_key.json "whoami && hostname"
"""
from __future__ import annotations
import os
import subprocess
import sys
from pathlib import Path
def main() -> int:
examples_dir = Path(__file__).resolve().parent
skill_root = examples_dir.parent
script = skill_root / "scripts" / "ssh_execute.py"
config_path = Path(sys.argv[1]).resolve() if len(sys.argv) >= 2 else (examples_dir / "config_single_key.json")
command = sys.argv[2] if len(sys.argv) >= 3 else "whoami && hostname"
if not script.exists():
raise FileNotFoundError(f"未找到脚本入口:{script}")
if not config_path.exists():
raise FileNotFoundError(f"未找到配置文件:{config_path}")
env = os.environ.copy()
env.setdefault("MSYS_NO_PATHCONV", "1")
proc = subprocess.run(
[sys.executable, str(script), str(config_path), command],
text=True,
env=env,
)
return proc.returncode
if __name__ == "__main__":
raise SystemExit(main())
"""
并发/批量场景示例(文档型)
建议把“批量执行/并发上传下载”收敛到固定入口,避免在不同项目里临时拼装命令。
"""
def main() -> None:
print(
"""
多服务器(servers 字典)选择某个环境执行:
python ../scripts/ssh_execute.py ./config_multi_servers.json "uptime" --server dev
如需对多台机器并发执行,请使用 ssh_cluster.py(更适合运维批量操作):
python ../scripts/ssh_cluster.py ./config_multi_servers.json "df -h"
"""
)
if __name__ == "__main__":
main()
{
"name": "example-env-development",
"summary": "示例:开发环境标注(environment=development)",
"host": "192.0.2.50",
"port": 22,
"user": "developer",
"key_file": "./keys/example_dev_id_ed25519",
"timeout": 15,
"strict_host_key_checking": false,
"auto_add_hostkey": true,
"use_controlmaster": true,
"control_persist": "5m",
"description": "开发环境配置示例 - 快速迭代使用",
"created_at": "2025-11-01T10:00:00+08:00",
"updated_at": "2025-11-01T10:00:00+08:00",
"environment": "development",
"tags": ["example", "development", "fast"],
"review_interval_days": 365,
"last_reviewed_at": "2025-11-01T10:00:00+08:00",
"owner": "(填:负责人)",
"contact": "user@example.invalid",
"notes": "开发环境通常允许更宽松的主机密钥检查,但生产环境建议启用严格检查。",
"config_version": "1.0"
}
{
"name": "example-env-production",
"summary": "示例:生产环境标注(environment=production,建议 strict_host_key_checking=true)",
"host": "198.51.100.50",
"port": 22,
"user": "deploy",
"key_file": "./keys/example_prod_id_ed25519",
"timeout": 30,
"strict_host_key_checking": true,
"auto_add_hostkey": false,
"use_controlmaster": true,
"control_persist": "30m",
"description": "生产环境配置示例 - 高安全性要求",
"created_at": "2025-01-15T09:00:00+08:00",
"updated_at": "2025-12-17T10:30:00+08:00",
"environment": "production",
"tags": ["example", "production", "secure"],
"review_interval_days": 90,
"last_reviewed_at": "2025-10-01T14:00:00+08:00",
"expires_at": null,
"owner": "(填:负责人)",
"contact": "user@example.invalid",
"team": "(填:团队)",
"notes": "生产环境建议:\n1. 启用严格主机密钥检查\n2. 禁用自动添加 hostkey,手动管理 known_hosts\n3. 定期审查配置\n4. 使用短期有效的访问凭证",
"documentation_url": "",
"config_version": "1.0"
}
{
"name": "example-jump-double-key",
"summary": "示例:双跳板机-密钥认证(替换 host/user/key_file 与 jump_hosts)",
"description": "双跳板机配置示例(连接深层内网服务器的典型场景)。",
"host": "192.0.2.30",
"port": 22,
"user": "dbadmin",
"key_file": "./keys/example_db_id_ed25519",
"timeout": 60,
"strict_host_key_checking": true,
"auto_add_hostkey": true,
"use_controlmaster": true,
"control_persist": "1h",
"jump_hosts": [
{
"name": "bastion-1",
"host": "198.51.100.30",
"port": 22,
"user": "jumpuser1",
"key_file": "./keys/example_jump1_id_ed25519",
"description": "第一级跳板机"
},
{
"name": "jump-2",
"host": "198.51.100.31",
"port": 22,
"user": "jumpuser2",
"key_file": "./keys/example_jump2_id_ed25519",
"description": "第二级跳板机"
}
],
"environment": "production",
"tags": ["example", "jump", "double", "key"],
"owner": "(填:负责人)",
"team": "(填:团队)",
"notes": "每层跳板机可配置不同 user/key_file;也可按实际情况简化为字符串格式。",
"config_version": "1.0"
}
{
"name": "example-jump-password",
"summary": "示例:跳板机 + 密码认证(替换 host/user/password 与 jump_hosts)",
"description": "密码认证 + 跳板机示例(全链路密码)。",
"host": "192.0.2.40",
"port": 22,
"user": "appuser",
"password": "__REPLACE_ME__",
"timeout": 30,
"jump_hosts": [
{
"host": "198.51.100.40",
"port": 22,
"user": "jumpuser",
"password": "__REPLACE_ME__"
}
],
"environment": "development",
"tags": ["example", "jump", "password"],
"owner": "(填:负责人)",
"team": "(填:团队)",
"notes": "注意:不要在仓库中提交真实密码;请使用安全方式注入。",
"config_version": "1.0"
}
{
"name": "example-jump-simple",
"summary": "示例:简化跳板机写法(jump_hosts 仅写 host 字符串)",
"description": "jump_hosts 仅写主机名,用户名/密钥默认沿用目标服务器配置。",
"host": "203.0.113.20",
"port": 22,
"user": "deploy",
"key_file": "./keys/example_deploy_id_ed25519",
"timeout": 30,
"strict_host_key_checking": true,
"auto_add_hostkey": true,
"use_controlmaster": true,
"control_persist": "30m",
"jump_hosts": ["bastion.example.invalid", "jump.internal.invalid"],
"environment": "production",
"tags": ["example", "jump", "simple"],
"config_version": "1.0"
}
{
"name": "example-jump-single-key",
"summary": "示例:单跳板机-密钥认证(替换 host/user/key_file 与 jump_hosts)",
"description": "单跳板机场景示例(密钥认证 + ControlMaster)。",
"host": "192.0.2.20",
"port": 22,
"user": "appuser",
"key_file": "./keys/example_app_id_ed25519",
"timeout": 30,
"strict_host_key_checking": true,
"auto_add_hostkey": true,
"use_controlmaster": true,
"control_persist": "30m",
"jump_hosts": [
{
"name": "bastion-1",
"host": "198.51.100.20",
"port": 22,
"user": "jumpuser",
"key_file": "./keys/example_jump_id_ed25519",
"description": "示例跳板机(外网入口)"
}
],
"environment": "production",
"tags": ["example", "jump", "single", "key"],
"owner": "(填:负责人)",
"team": "(填:团队)",
"notes": "请分别配置目标机与跳板机的密钥路径(key_file)。",
"config_version": "1.0"
}
{
"name": "example-key-with-passphrase",
"summary": "示例:单机-密钥认证-私钥带密码(替换 host/user/key_file/key_passphrase)",
"description": "私钥带密码的密钥认证示例(建议配合 ssh-agent 或安全的密钥管理方案)。",
"host": "198.51.100.10",
"port": 22,
"user": "deploy",
"key_file": "./keys/example_id_ed25519",
"key_passphrase": "__REPLACE_ME__",
"timeout": 30,
"strict_host_key_checking": true,
"auto_add_hostkey": true,
"use_controlmaster": true,
"control_persist": "10m",
"environment": "production",
"tags": ["example", "key", "passphrase"],
"owner": "(填:负责人)",
"team": "(填:团队)",
"notes": "注意:不要在仓库中提交真实 key_passphrase;请使用安全方式注入。",
"config_version": "1.0"
}
{
"summary": "示例:多服务器配置(servers 字典),用于批量执行/并发/集群操作。",
"description": "多环境服务器配置示例(请替换各 server 的 host/user/key_file)。",
"servers": {
"dev": {
"name": "dev-server",
"summary": "示例:dev-开发环境-密钥认证",
"description": "开发环境服务器 - 用于日常开发和测试",
"host": "192.0.2.61",
"port": 22,
"user": "dev",
"key_file": "../keys/example_dev_id_ed25519",
"timeout": 30,
"use_controlmaster": true,
"control_persist": "10m",
"environment": "development",
"tags": ["example", "dev"]
},
"staging": {
"name": "staging-server",
"summary": "示例:staging-预发布-密钥认证",
"description": "预发布环境服务器 - 上线前验证",
"host": "198.51.100.61",
"port": 22,
"user": "deploy",
"key_file": "../keys/example_staging_id_ed25519",
"timeout": 30,
"use_controlmaster": true,
"control_persist": "30m",
"environment": "staging",
"tags": ["example", "staging"]
},
"prod": {
"name": "prod-server",
"summary": "示例:prod-生产-密钥认证",
"description": "生产环境主服务器(示例域名)",
"host": "prod.example.invalid",
"port": 22,
"user": "deploy",
"key_file": "../keys/example_prod_id_ed25519",
"timeout": 60,
"use_controlmaster": true,
"control_persist": "1h",
"strict_host_key_checking": true,
"environment": "production",
"tags": ["example", "prod"]
}
},
"config_version": "1.0"
}
{
"name": "example-single-key",
"summary": "示例:开发环境-单机-密钥认证(替换 host/user/key_file)",
"description": "单机密钥认证示例,默认启用 ControlMaster 连接复用。",
"host": "192.0.2.10",
"port": 22,
"user": "deploy",
"key_file": "./keys/example_id_ed25519",
"timeout": 30,
"strict_host_key_checking": true,
"auto_add_hostkey": true,
"use_controlmaster": true,
"control_persist": "10m",
"jump_hosts": [],
"environment": "development",
"tags": ["example", "single", "key"],
"owner": "(填:负责人)",
"team": "(填:团队)",
"notes": "请将 key_file 指向实际私钥路径,并确保权限正确(建议 chmod 600)。",
"config_version": "1.0"
}
{
"name": "example-single-password",
"summary": "示例:单机-密码认证(替换 host/user/password)",
"description": "密码认证示例(走 Paramiko 客户端连接池),适合无法使用密钥认证的场景。",
"host": "203.0.113.10",
"port": 22,
"user": "admin",
"password": "__REPLACE_ME__",
"timeout": 30,
"strict_host_key_checking": true,
"auto_add_hostkey": true,
"environment": "development",
"tags": ["example", "single", "password"],
"owner": "(填:负责人)",
"team": "(填:团队)",
"notes": "建议:优先使用密钥认证;如必须使用密码,请用安全方案注入,不要明文提交。",
"config_version": "1.0"
}
{
"name": "TEMPLATE",
"summary": "(示例)中文摘要:用途/环境/责任人/系统,例如:生产-支付网关-上海机房-张三",
"host": "YOUR_SERVER_IP_OR_HOSTNAME",
"port": 22,
"user": "YOUR_USERNAME",
"key_file": "./YOUR_KEY_FILE_PATH",
"key_passphrase": null,
"timeout": 30,
"strict_host_key_checking": false,
"use_controlmaster": true,
"control_persist": "10m",
"jump_hosts": [],
"description": "配置文件模板 - 请修改后使用",
"created_at": "2025-12-17T12:00:00+08:00",
"updated_at": "2025-12-17T12:00:00+08:00",
"environment": "development",
"tags": ["template", "example"],
"review_interval_days": 90,
"last_reviewed_at": "2025-12-17T12:00:00+08:00",
"expires_at": null,
"owner": "YOUR_NAME",
"contact": "YOUR_EMAIL_OR_CONTACT",
"team": "YOUR_TEAM",
"notes": "这是配置模板,请根据实际情况修改所有字段。\n\n元数据字段说明:\n- created_at/updated_at: 自动管理的时间戳\n- environment: production/staging/development/testing\n- tags: 便于分类和筛选\n- review_interval_days: 审查间隔(生产环境建议90天)\n- expires_at: 临时访问配置的过期时间\n- owner/contact/team: 团队协作信息",
"documentation_url": "",
"config_version": "1.0"
}
{
"name": "example-temporary-access",
"summary": "示例:临时访问配置(expires_at 到期后应拒绝执行)",
"host": "203.0.113.60",
"port": 22,
"user": "contractor",
"key_file": "./keys/example_temp_id_ed25519",
"timeout": 30,
"use_controlmaster": true,
"control_persist": "10m",
"description": "临时访问配置示例 - 自动过期",
"created_at": "2025-12-01T09:00:00+08:00",
"updated_at": "2025-12-01T09:00:00+08:00",
"environment": "staging",
"tags": ["example", "temporary", "expires"],
"review_interval_days": 30,
"last_reviewed_at": "2025-12-01T09:00:00+08:00",
"expires_at": "2025-12-31T23:59:59+08:00",
"owner": "(填:负责人)",
"contact": "user@example.invalid",
"team": "(填:团队)",
"notes": "此配置用于临时访问,过期后应自动拒绝执行。项目结束后应立即删除。",
"config_version": "1.0"
}
"""
配置加载示例(文档型)
本文件用于给人/AI 提供“正确范式”,避免在不同项目中手写 `ssh/scp`。
更完整的索引请看:README.md
"""
def main() -> None:
print(
"""
最常见用法(推荐:走 CLI 入口,稳定且不依赖 Python 导入路径):
# 单机(密钥认证)
python ../scripts/ssh_execute.py ./config_single_key.json "whoami && hostname"
# 单跳板机
python ../scripts/ssh_execute.py ./config_jump_single_key.json "uptime"
# 多服务器(选择某个 server)
python ../scripts/ssh_execute.py ./config_multi_servers.json "hostname" --server dev
配置文件建议字段:
- summary(中文摘要,推荐必填):一眼看懂用途/环境/责任人
- description(可选):补充说明
- environment/tags/owner/team(可选):便于团队协作与筛选
"""
)
if __name__ == "__main__":
main()
"""
交互式会话提示(文档型)
运维中常见的交互问题:
- `sudo` 需要输入密码
- `apt/yum` 需要确认
- 远端脚本提示 `Enter password:` / `Press any key`
建议:
- 优先把命令改成“非交互式”(例如 `DEBIAN_FRONTEND=noninteractive`、`-y`、`sudo -n` 等)
- 若必须交互,建议把交互步骤拆成“明确的多步命令”并逐步验证输出
本 skill 的核心目标是:把连接与复用封装起来,避免回退到手写 `ssh`。
"""
def main() -> None:
print(
"""
示例:把 apt 安装变为非交互式(需要你按真实系统调整):
python ../scripts/ssh_execute.py ./config_single_key.json "sudo -n true && echo ok || echo 'sudo 需要免密或交互'"
python ../scripts/ssh_execute.py ./config_single_key.json "sudo -n DEBIAN_FRONTEND=noninteractive apt-get update -y"
python ../scripts/ssh_execute.py ./config_single_key.json "sudo -n DEBIAN_FRONTEND=noninteractive apt-get install -y nginx"
"""
)
if __name__ == "__main__":
main()
"""
跳板机场景示例(文档型)
目标:把“直连/单跳/双跳/简化跳板”四类典型拓扑固定成模板,减少跑偏与试错。
"""
def main() -> None:
print(
"""
单跳板机(推荐:使用 config_jump_single_key.json):
python ../scripts/ssh_execute.py ./config_jump_single_key.json "whoami && hostname"
双跳板机(推荐:使用 config_jump_double.json):
python ../scripts/ssh_execute.py ./config_jump_double.json "whoami && hostname"
简化写法(jump_hosts 只写字符串主机名):
python ../scripts/ssh_execute.py ./config_jump_simple.json "uptime"
注意:
- 示例配置全部是占位符/保留网段 IP,请按你的真实环境替换后使用。
- 遇到需要一次性初始化的步骤(例如写入 authorized_keys)可以临时用系统 ssh,但后续日常操作必须回到本 skill。
"""
)
if __name__ == "__main__":
main()
examples(回归用例集)
本目录用于保存 ssh-skill 的可运行示例与配置模板,作为运维场景的“回归用例集”:
- 给人看:快速复制改造,减少手工写
ssh/scp的概率 - 给 AI 看:提供明确范式,减少跑偏、减少反复试错
重要安全约定
- 本目录所有
*.json都是示例模板:只使用占位符或保留网段 IP(如192.0.2.x、198.51.100.x、203.0.113.x),禁止写入真实服务器信息/密钥/密码。 - 密码字段只允许占位符(例如
__REPLACE_ME__),不要提交真实密码。
配置示例(JSON)
config_template.json:通用模板(包含summary字段示例)config_single_key.json:单机 + 密钥认证 + ControlMaster(推荐默认)config_key_with_passphrase.json:单机 + 密钥认证(带私钥密码)config_single_password.json:单机 + 密码认证(走 Paramiko 客户端连接池)config_jump_single_key.json:单跳板机 + 密钥认证config_jump_double.json:双跳板机 + 密钥认证config_jump_simple.json:简化跳板机写法(jump_hosts仅写字符串)config_jump_password.json:跳板机 + 密码认证(全链路密码示例)config_multi_servers.json:多服务器配置(servers字典)config_env_development.json:环境示例(开发)config_env_production.json:环境示例(生产)config_temporary_access.json:临时访问(含expires_at示例)
示例脚本(Python)
这些脚本主要用于演示如何用 scripts/lib/ 的 API 进行连接、并发与交互:
basic_usage.py:最小用法concurrency_examples.py:并发/批量jumphost_usage_examples.py:跳板机场景interactive_session_examples.py:交互式会话范式config_usage_examples.py:如何加载配置文件test_controlmaster.py:ControlMaster 复用行为演示
静态冒烟测试(推荐)
用于确保示例 JSON 长期保持可用(结构正确、元数据齐全),不需要真实连服务器:
pwsh -NoProfile -ExecutionPolicy Bypass -File .\\run_smoke_tests.ps1如只想做结构校验、不强制元数据:
pwsh -NoProfile -ExecutionPolicy Bypass -File .\\run_smoke_tests.ps1 -StrictMetadata:$falseparam(
[switch] $StrictMetadata = $true
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$examplesDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$skillRoot = Split-Path -Parent $examplesDir
$manager = Join-Path $skillRoot 'scripts\\ssh_config_manager.py'
if (-not (Test-Path -LiteralPath $manager)) {
throw "未找到 ssh_config_manager.py:$manager"
}
$python = Get-Command python -ErrorAction SilentlyContinue
if (-not $python) {
$python = Get-Command py -ErrorAction SilentlyContinue
}
if (-not $python) {
throw "未找到 Python(python/py)。请先安装 Python,并确保可在 PATH 中调用。"
}
$jsonFiles = Get-ChildItem -LiteralPath $examplesDir -File -Filter *.json | Sort-Object Name
if (-not $jsonFiles) {
Write-Host "未找到示例 JSON(跳过):$examplesDir"
exit 0
}
$failed = New-Object System.Collections.Generic.List[string]
foreach ($file in $jsonFiles) {
Write-Host "校验:$($file.Name)"
$args = @($manager, 'validate', $file.FullName)
if ($StrictMetadata) {
$args += '--strict-metadata'
}
& $python @args | Write-Host
if ($LASTEXITCODE -ne 0) {
$failed.Add($file.Name) | Out-Null
}
}
if ($failed.Count -gt 0) {
Write-Host "示例冒烟测试失败:$($failed.Count) 个文件"
$failed | ForEach-Object { Write-Host " - $_" }
exit 1
}
Write-Host "示例冒烟测试通过:$($jsonFiles.Count) 个文件"
exit 0
"""
ControlMaster 复用性能对比(可选回归)
说明:
- 该脚本用于直观展示“每次新连接” vs “ControlMaster 复用”差异。
- 需要你把 HOST/USER/KEY_FILE 替换成可用目标。
"""
from __future__ import annotations
import os
import subprocess
import time
# 测试配置(请替换)
HOST = "192.0.2.10"
USER = "deploy"
KEY_FILE = "./keys/example_id_ed25519"
ITERATIONS = 10
def _user_known_hosts_file_arg() -> str:
return "UserKnownHostsFile=NUL" if os.name == "nt" else "UserKnownHostsFile=/dev/null"
def test_without_controlmaster() -> float:
print("\n" + "=" * 60)
print("测试1: 传统方式(每次建立新连接)")
print("=" * 60)
times_ms = []
for i in range(ITERATIONS):
cmd = [
"ssh",
"-i",
KEY_FILE,
"-o",
"StrictHostKeyChecking=no",
"-o",
_user_known_hosts_file_arg(),
f"{USER}@{HOST}",
"echo test",
]
start = time.time()
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
elapsed = (time.time() - start) * 1000
times_ms.append(elapsed)
print(f" 第{i + 1:2d}次: {elapsed:6.1f}ms - {result.stdout.strip()}")
avg = sum(times_ms) / len(times_ms)
print(f"\n 平均耗时: {avg:.1f}ms")
return avg
def test_with_controlmaster() -> float:
print("\n" + "=" * 60)
print("测试2: ControlMaster方式(连接复用)")
print("=" * 60)
control_path = os.path.join(os.environ.get("TEMP", "/tmp"), f"ssh-test-{USER}@{HOST}")
if os.path.exists(control_path):
try:
os.remove(control_path)
except OSError:
pass
times_ms = []
for i in range(ITERATIONS):
cmd = [
"ssh",
"-i",
KEY_FILE,
"-o",
"StrictHostKeyChecking=no",
"-o",
_user_known_hosts_file_arg(),
"-o",
"ControlMaster=auto",
"-o",
f"ControlPath={control_path}",
"-o",
"ControlPersist=10s",
f"{USER}@{HOST}",
"echo test",
]
start = time.time()
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
elapsed = (time.time() - start) * 1000
times_ms.append(elapsed)
print(f" 第{i + 1:2d}次: {elapsed:6.1f}ms - {result.stdout.strip()}")
avg = sum(times_ms) / len(times_ms)
print(f"\n 平均耗时: {avg:.1f}ms")
return avg
def main() -> None:
avg1 = test_without_controlmaster()
avg2 = test_with_controlmaster()
if avg2 > 0:
print(f"\n加速比(大致):{avg1 / avg2:.1f}x")
if __name__ == "__main__":
main()
SSH Skill - High-Performance SSH Operations Tool
中文 | English
Enterprise-grade SSH management tool for Codex / Claude Code, making remote server operations as simple and efficient as local ones
 
📢 Recent Updates
v3.3.1 - Codex Metadata Compatibility Fix (2026-05-01)
- 🧩 Codex compatibility: Shortened the
SKILL.mdfrontmatterdescriptionto stay below Codex's 1024-character limit and prevent skill loading failures - 🛡️ Trigger semantics preserved: Kept the critical SSH/server operations trigger, raw
ssh/scpprohibition, and core server/jump-host/transfer/tunnel/database/internal-access keywords
v3.3 - Windows Native SSH Adaptation & Passphrase Key Support (2026-03-24)
- 🔑 Full Passphrase Key Support: Seamless passphrase-protected key usage through Windows SSH Agent integration — no interactive password input needed
- 🪟 Windows Native SSH Adaptation: Auto-locates
%SystemRoot%\System32\OpenSSH\ssh.exe, resolving PATH priority conflicts between Git SSH and Windows native SSH - 🔌 SSH Tunnel Management: Local port forwarding with daemon mode, auto-reconnect, heartbeat detection — easy access to remote databases and internal services
- 🛡️ Windows SSH Agent Tool: One-click detection, startup, and configuration of the Windows OpenSSH Authentication Agent service
✨ Core Features
🚀 Ultimate Performance
Daemon Long-Connection Mode - Industry-leading performance optimization
| Mode | Single Command | 10 Commands | 30 Commands | Performance Gain |
|---|---|---|---|---|
| Traditional Direct | ~0.45s | ~4.5s | ~13.5s | - |
| Daemon Mode | ~0.12s | ~1.2s | ~3.6s | 🔥 3.75x |
- Auto-start daemon on first connection
- Multiple Claude Code instances share connections
- Automatic heartbeat detection and reconnection
- Auto-exit after 30 minutes idle
📊 Smart Large File Transfer
Automatic Transfer Mode Switching - Intelligently selects optimal solution based on file size
File size ≤ 80MB → Native SCP (fast completion)
File size > 80MB → Paramiko SFTP (real-time progress)Real-time Progress Display:
{
"file": "large-file.iso",
"total": 310984990,
"transferred": 155492495,
"percent": 50.0,
"speed": "2.1 MB/s",
"eta": 74.2
}Smart Timeout Calculation:
- Auto-calculate timeout based on file size
- Formula:
File size(MB) ÷ 1MB/s + 60s buffer - Range: 60 seconds - 3600 seconds (1 hour)
Transfer Optimization:
- Block size: 128KB (4x performance boost)
- Resume support
- No timeout limit (large files)
- Recursive directory upload/download
🌐 Server-to-Server Direct Transfer
Zero Local Bandwidth Consumption - Data transfers directly between servers
# Auto mode (recommended) - intelligently selects optimal method
ssh_server_transfer.py source-server /data/backup.tar.gz target-server /backup/
# Direct mode - recommended for large files (data doesn't go through local)
ssh_server_transfer.py source-server /data/large.iso target-server /data/ --mode direct
# Support rsync incremental sync
ssh_server_transfer.py source-server /data/ target-server /backup/ --use-rsyncTransfer Mode Comparison:
| Mode | Data Flow | Use Case | Advantages |
|---|---|---|---|
| Direct | Source → Target | Large files, servers connected | Fast, no local bandwidth |
| Stream | Source → Local → Target | Small files, network issues | No server-to-server config needed |
| Hybrid | Try direct first, fallback to stream | Uncertain environment | Auto-adaptive |
| Auto | Smart decision | Default | Optimal choice |
🎯 Jump Host Support
Multi-level Jump Host Auto-handling - Using standard ProxyJump
Host internal-server
HostName 10.0.1.100
User appuser
ProxyJump bastion1,bastion2AI only needs to know the internal-server alias, multi-level jumping is handled automatically.
🔧 Unified Configuration Management
Based on Standard OpenSSH Config - Compatible with all SSH tools
# List all servers
ssh_config_manager_v3.py list-servers
# Find servers
ssh_config_manager_v3.py find "web"
# Create configuration
ssh_config_manager_v3.py create --alias prod-web-01 --host 192.168.1.100 --user root
# Update configuration
ssh_config_manager_v3.py update prod-web-01 --description "Production Web Server"Metadata Support:
- Environment tags (production/development/staging)
- Location information
- Custom tags
- Creation/update timestamps
⚡ Batch Concurrent Operations
Execute commands on multiple servers concurrently
# Execute on all servers
ssh_cluster.py "uptime" --parallel
# Filter by environment
ssh_cluster.py "systemctl status nginx" --environment production --parallel
# Filter by tags
ssh_cluster.py "df -h" --tags "web,nginx" --parallel --max-workers 10📦 Installation
Dependencies
pip install paramikoConfiguration
1. For Codex: place the ssh-skill directory under C:\Users\<username>\.agents\skills\ssh-skill\ or your active Codex skills directory. 2. For Claude Code: place the ssh-skill directory under ~/.claude/skills/ssh-skill/. 3. Configure SSH key or password authentication. 4. Start using.
🎬 Quick Start
Execute Remote Commands
python ~/.claude/skills/ssh-skill/scripts/ssh_execute.py prod-web-01 "systemctl status nginx"Upload Files
# Small files (fast)
MSYS_NO_PATHCONV=1 python ~/.claude/skills/ssh-skill/scripts/ssh_upload.py prod-web-01 ./app.tar.gz /tmp/
# Large files (auto progress display)
MSYS_NO_PATHCONV=1 python ~/.claude/skills/ssh-skill/scripts/ssh_upload.py prod-web-01 ./large-file.iso /tmp/
# Resume support
MSYS_NO_PATHCONV=1 python ~/.claude/skills/ssh-skill/scripts/ssh_upload.py prod-web-01 ./large-file.iso /tmp/ --resume
# Recursive directory upload
MSYS_NO_PATHCONV=1 python ~/.claude/skills/ssh-skill/scripts/ssh_upload.py prod-web-01 ./dist/ /var/www/html/ --recursiveDownload Files
MSYS_NO_PATHCONV=1 python ~/.claude/skills/ssh-skill/scripts/ssh_download.py prod-web-01 /var/log/app.log ./app.logServer-to-Server Transfer
MSYS_NO_PATHCONV=1 python ~/.claude/skills/ssh-skill/scripts/ssh_server_transfer.py source-server /data/backup.tar.gz target-server /backup/🎯 Use Cases
Scenario 1: Daily Operations
# Quick server status check
ssh_execute.py web-01 "uptime && free -m && df -h"
# Batch service restart
ssh_cluster.py "systemctl restart nginx" --environment production --parallelScenario 2: Large File Deployment
# Upload 500MB application package (auto progress display)
ssh_upload.py prod-web-01 ./app-v2.0.tar.gz /opt/apps/
# Output example:
# Upload progress: 45.2% (2.1 MB/s) ETA: 102.3sScenario 3: Data Migration
# Server-to-server direct transfer (no local bandwidth)
ssh_server_transfer.py old-server /data/database.sql new-server /data/ --mode direct
# Use rsync for incremental sync
ssh_server_transfer.py source /data/ target /backup/ --use-rsyncScenario 4: Jump Host Access
# Access internal server through jump host (auto-handled)
ssh_execute.py internal-server "docker ps"📈 Performance Data
Real Test Data
Test Environment:
- File size: 297MB
- Network speed: 1.6-2.1 MB/s
- Server: test-001
Test Results:
| Metric | Native SCP | Paramiko SFTP | Advantage |
|---|---|---|---|
| Progress Display | ❌ None | ✅ Real-time | User Experience |
| Timeout Issue | ❌ 30s fixed | ✅ Unlimited | Stability |
| Resume Support | ❌ Not supported | ✅ Supported | Reliability |
| Transfer Speed | Fast | Slightly slower | Performance |
Smart Selection Strategy:
- File ≤ 80MB: Use native SCP (fast completion)
- File > 80MB: Use Paramiko SFTP (real-time progress)
Daemon Performance
Command Execution Speed Comparison:
Traditional Mode:
Command 1: 0.45s
Command 2: 0.45s
Command 3: 0.45s
Total: 1.35s
Daemon Mode:
Command 1: 0.45s (first start daemon)
Command 2: 0.12s (reuse connection)
Command 3: 0.12s (reuse connection)
Total: 0.69s (1.96x improvement)🔐 Security Features
- Support key and password authentication
- Password encrypted storage in SSH config comments
- Support key password protection
- Auto-add host keys (configurable)
- Support SSH agent forwarding
🛠️ Advanced Features
Resume Support
# Upload large file, support resume after interruption
ssh_upload.py prod-web-01 ./large-file.iso /tmp/ --resumeRecursive Directory Transfer
# Recursively upload entire directory
ssh_upload.py prod-web-01 ./dist/ /var/www/html/ --recursiveAuto Error Recovery
- SSH connection auto-reconnect on disconnect (max 3 times)
- Heartbeat detection every 60 seconds
- Auto-retry on transfer failure
Configuration Management
# Filter by environment
ssh_config_manager_v3.py list-servers --environment production
# Filter by tags
ssh_config_manager_v3.py list-servers --tags web,nginx
# Update server info
ssh_config_manager_v3.py update prod-web-01 --description "New description" --tags tag1,tag2📚 Configuration Examples
Key Authentication
# ===== prod-web-01 =====
# description: Production Web Server
# environment: production
# tags: web,nginx,production
# location: Alibaba Cloud - Beijing
Host prod-web-01
HostName 192.168.1.100
User root
IdentityFile ~/.ssh/id_rsa
Port 22Password Authentication
# ===== dev-server =====
# description: Development Server
# environment: development
# password: your-password
Host dev-server
HostName 192.168.1.200
User root
Port 22Jump Host Configuration
Host bastion
HostName bastion.example.com
User jumpuser
IdentityFile ~/.ssh/jump_key
Host internal-server
HostName 10.0.1.100
User appuser
IdentityFile ~/.ssh/id_rsa
ProxyJump bastion🎨 Integration with Codex / Claude Code
In Codex or Claude Code, AI uses the SKILL.md description to automatically select ssh-skill for SSH operations:
User: Check Nginx status on prod-web-01
AI: [Auto-calls ssh_execute.py]
User: Upload app.tar.gz to /tmp on prod-web-01
AI: [Auto-calls ssh_upload.py]
User: Migrate data from old-server to new-server
AI: [Auto-calls ssh_server_transfer.py]🔄 Version History
v3.3.1 (2026-05-01)
- 🧩 Codex metadata compatibility fix: Shortened the
SKILL.mdfrontmatterdescriptionto avoid Codex skill loading failures caused by the 1024-character description limit - 🛡️ Trigger semantics preserved: Kept the critical SSH/server operations trigger, raw
ssh/scpprohibition, and core server/jump-host/transfer/tunnel/database/internal-access keywords
v3.2 (2026-03-04)
- ✨ Large file transfer optimization: Smart mode switching (80MB threshold)
- ✨ Real-time progress display: Percentage, speed, ETA
- ✨ Smart timeout calculation: Auto-calculate based on file size
- ✨ Block size optimization: 32KB → 128KB (4x improvement)
- ✨ No timeout limit: Large file transfers no longer timeout
v3.1
- Daemon long-connection mode
- Server-to-server direct transfer
- Batch concurrent operations
- Unified configuration management
v3.0
- Based on OpenSSH config
- Jump host support
- Metadata management
🤝 Contributing
Issues and Pull Requests are welcome!
📄 License
MIT License
👨💻 Author
Michael Zhang - @badseal
---
Making remote server operations as simple and efficient as local ones! 🚀
SSH Skill - 高性能 SSH 操作技能
中文 | English
为 Codex / Claude Code 打造的企业级 SSH 管理工具,让远程服务器操作像本地一样简单高效
 
📢 最近更新
v3.3.1 - Codex 元数据兼容修复(2026-05-01)
- 🧩 Codex 兼容:压缩
SKILL.mdfrontmatter 的description,避免超过 Codex 1024 字符限制导致 skill 无法加载 - 🛡️ 触发语义保留:保留 SSH/server operations 强触发、禁止直接
ssh/scp、服务器/跳板机/传输/隧道/数据库内网访问等核心关键词
v3.3 - Windows 原生 SSH 适配 & Passphrase 密钥支持(2026-03-24)
- 🔑 Passphrase 密钥完整支持:通过 Windows SSH Agent 集成,passphrase 保护的密钥可以无感使用,无需每次交互输入密码
- 🪟 Windows 原生 SSH 适配:自动定位
%SystemRoot%\System32\OpenSSH\ssh.exe,解决 Git SSH 与 Windows 原生 SSH 的 PATH 优先级冲突 - 🔌 SSH 隧道管理:新增本地端口转发功能,支持守护进程模式、自动重连、心跳检测,可便捷访问远程数据库和内网服务
- 🛡️ Windows SSH Agent 工具:一键检测、启动和配置 Windows OpenSSH Authentication Agent 服务
✨ 核心特性
🚀 极致性能
守护进程长连接模式 - 业界领先的性能优化
| 模式 | 单次命令 | 连续 10 条 | 连续 30 条 | 性能提升 |
|---|---|---|---|---|
| 传统直连 | ~0.45s | ~4.5s | ~13.5s | - |
| 守护进程 | ~0.12s | ~1.2s | ~3.6s | 🔥 3.75x |
- 首次连接自动启动守护进程
- 多个 Claude Code 实例共享连接
- 自动心跳检测和断线重连
- 空闲 30 分钟自动退出
📊 智能大文件传输
自动切换传输模式 - 根据文件大小智能选择最优方案
文件大小 ≤ 80MB → 原生 SCP(快速完成)
文件大小 > 80MB → Paramiko SFTP(实时进度)实时进度显示:
{
"file": "large-file.iso",
"total": 310984990,
"transferred": 155492495,
"percent": 50.0,
"speed": "2.1 MB/s",
"eta": 74.2
}智能超时计算:
- 根据文件大小自动计算超时时间
- 公式:
文件大小(MB) ÷ 1MB/s + 60秒缓冲 - 范围:60 秒 - 3600 秒(1小时)
传输优化:
- 块大小:128KB(4倍性能提升)
- 支持断点续传
- 无超时限制(大文件)
- 目录递归上传/下载
🌐 服务器间直接传输
零本地带宽消耗 - 数据直接在服务器间传输
# 自动模式(推荐)- 智能选择最优方式
ssh_server_transfer.py source-server /data/backup.tar.gz target-server /backup/
# 直连模式 - 大文件推荐(数据不经过本地)
ssh_server_transfer.py source-server /data/large.iso target-server /data/ --mode direct
# 支持 rsync 增量同步
ssh_server_transfer.py source-server /data/ target-server /backup/ --use-rsync传输模式对比:
| 模式 | 数据流向 | 适用场景 | 优势 |
|---|---|---|---|
| 直连 (direct) | 源 → 目标 | 大文件、服务器间网络通 | 速度快,不占本地带宽 |
| 流式 (stream) | 源 → 本地 → 目标 | 小文件、网络不通 | 无需服务器间配置 |
| 混合 (hybrid) | 先尝试直连,失败降级 | 不确定环境 | 自动适应 |
| 自动 (auto) | 智能判断 | 默认 | 最优选择 |
🎯 跳板机支持
多级跳板机自动处理 - 使用标准 ProxyJump
Host internal-server
HostName 10.0.1.100
User appuser
ProxyJump bastion1,bastion2AI 只需要知道 internal-server 别名,底层自动处理多级跳转。
🔧 统一配置管理
基于标准 OpenSSH 配置 - 兼容所有 SSH 工具
# 列出所有服务器
ssh_config_manager_v3.py list-servers
# 查找服务器
ssh_config_manager_v3.py find "web"
# 创建配置
ssh_config_manager_v3.py create --alias prod-web-01 --host 192.168.1.100 --user root
# 更新配置
ssh_config_manager_v3.py update prod-web-01 --description "生产环境 Web 服务器"元数据支持:
- 环境标签(production/development/staging)
- 位置信息
- 自定义标签
- 创建/更新时间
⚡ 批量并发操作
对多台服务器并发执行命令
# 对所有服务器执行
ssh_cluster.py "uptime" --parallel
# 按环境过滤
ssh_cluster.py "systemctl status nginx" --environment production --parallel
# 按标签过滤
ssh_cluster.py "df -h" --tags "web,nginx" --parallel --max-workers 10📦 安装
依赖
pip install paramiko配置
1. Codex 用户:将 ssh-skill 目录放到 C:\Users\<用户名>\.agents\skills\ssh-skill\ 或当前 Codex 技能目录。 2. Claude Code 用户:将 ssh-skill 目录放到 ~/.claude/skills/ssh-skill/。 3. 配置 SSH 密钥或密码认证。 4. 开始使用。
🎬 快速开始
执行远程命令
python ~/.claude/skills/ssh-skill/scripts/ssh_execute.py prod-web-01 "systemctl status nginx"上传文件
# 小文件(快速)
MSYS_NO_PATHCONV=1 python ~/.claude/skills/ssh-skill/scripts/ssh_upload.py prod-web-01 ./app.tar.gz /tmp/
# 大文件(自动显示进度)
MSYS_NO_PATHCONV=1 python ~/.claude/skills/ssh-skill/scripts/ssh_upload.py prod-web-01 ./large-file.iso /tmp/
# 断点续传
MSYS_NO_PATHCONV=1 python ~/.claude/skills/ssh-skill/scripts/ssh_upload.py prod-web-01 ./large-file.iso /tmp/ --resume
# 递归上传目录
MSYS_NO_PATHCONV=1 python ~/.claude/skills/ssh-skill/scripts/ssh_upload.py prod-web-01 ./dist/ /var/www/html/ --recursive下载文件
MSYS_NO_PATHCONV=1 python ~/.claude/skills/ssh-skill/scripts/ssh_download.py prod-web-01 /var/log/app.log ./app.log服务器间传输
MSYS_NO_PATHCONV=1 python ~/.claude/skills/ssh-skill/scripts/ssh_server_transfer.py source-server /data/backup.tar.gz target-server /backup/🎯 使用场景
场景 1:日常运维
# 快速检查服务器状态
ssh_execute.py web-01 "uptime && free -m && df -h"
# 批量重启服务
ssh_cluster.py "systemctl restart nginx" --environment production --parallel场景 2:大文件部署
# 上传 500MB 应用包(自动显示进度)
ssh_upload.py prod-web-01 ./app-v2.0.tar.gz /opt/apps/
# 输出示例:
# 上传进度: 45.2% (2.1 MB/s) ETA: 102.3s场景 3:数据迁移
# 服务器间直接传输(不占用本地带宽)
ssh_server_transfer.py old-server /data/database.sql new-server /data/ --mode direct
# 使用 rsync 增量同步
ssh_server_transfer.py source /data/ target /backup/ --use-rsync场景 4:跳板机访问
# 通过跳板机访问内网服务器(自动处理)
ssh_execute.py internal-server "docker ps"📈 性能数据
真实测试数据
测试环境:
- 文件大小:297MB
- 网络速度:1.6-2.1 MB/s
- 服务器:test-001
测试结果:
| 指标 | 原生 SCP | Paramiko SFTP | 优势 |
|---|---|---|---|
| 进度显示 | ❌ 无 | ✅ 实时 | 用户体验 |
| 超时问题 | ❌ 30秒固定 | ✅ 无限制 | 稳定性 |
| 断点续传 | ❌ 不支持 | ✅ 支持 | 可靠性 |
| 传输速度 | 快 | 稍慢 | 性能 |
智能选择策略:
- 文件 ≤ 80MB:使用原生 SCP(快速完成)
- 文件 > 80MB:使用 Paramiko SFTP(实时进度)
守护进程性能
命令执行速度对比:
传统模式:
命令 1: 0.45s
命令 2: 0.45s
命令 3: 0.45s
总计: 1.35s
守护进程模式:
命令 1: 0.45s (首次启动守护进程)
命令 2: 0.12s (复用连接)
命令 3: 0.12s (复用连接)
总计: 0.69s (提升 1.96x)🔐 安全特性
- 支持密钥认证和密码认证
- 密码加密存储在 SSH 配置注释中
- 支持密钥密码保护
- 自动添加主机密钥(可配置)
- 支持 SSH agent forwarding
🛠️ 高级功能
断点续传
# 上传大文件,支持中断后继续
ssh_upload.py prod-web-01 ./large-file.iso /tmp/ --resume目录递归传输
# 递归上传整个目录
ssh_upload.py prod-web-01 ./dist/ /var/www/html/ --recursive自动错误恢复
- SSH 连接断开自动重连(最多 3 次)
- 每 60 秒心跳检测连接状态
- 传输失败自动重试
配置管理
# 按环境过滤
ssh_config_manager_v3.py list-servers --environment production
# 按标签过滤
ssh_config_manager_v3.py list-servers --tags web,nginx
# 更新服务器信息
ssh_config_manager_v3.py update prod-web-01 --description "新描述" --tags tag1,tag2📚 配置示例
密钥认证
# ===== prod-web-01 =====
# description: 生产环境 Web 服务器
# environment: production
# tags: web,nginx,production
# location: 阿里云-北京
Host prod-web-01
HostName 192.168.1.100
User root
IdentityFile ~/.ssh/id_rsa
Port 22密码认证
# ===== dev-server =====
# description: 开发服务器
# environment: development
# password: your-password
Host dev-server
HostName 192.168.1.200
User root
Port 22跳板机配置
Host bastion
HostName bastion.example.com
User jumpuser
IdentityFile ~/.ssh/jump_key
Host internal-server
HostName 10.0.1.100
User appuser
IdentityFile ~/.ssh/id_rsa
ProxyJump bastion🎨 与 Codex / Claude Code 集成
在 Codex 或 Claude Code 中,AI 会根据 SKILL.md 的描述自动使用 ssh-skill 处理 SSH 操作:
用户:在 prod-web-01 上检查 Nginx 状态
AI:[自动调用 ssh_execute.py]
用户:上传 app.tar.gz 到 prod-web-01 的 /tmp 目录
AI:[自动调用 ssh_upload.py]
用户:从 old-server 迁移数据到 new-server
AI:[自动调用 ssh_server_transfer.py]🔄 版本历史
v3.3.1 (2026-05-01)
- 🧩 Codex 元数据兼容修复:压缩
SKILL.mdfrontmatterdescription,避免超过 Codex 1024 字符限制导致 skill 加载失败 - 🛡️ 触发语义保留:保留 SSH/server operations 强触发、禁止直接
ssh/scp、服务器/跳板机/传输/隧道/数据库内网访问等核心关键词
v3.2 (2026-03-04)
- ✨ 大文件传输优化:智能切换传输模式(80MB 阈值)
- ✨ 实时进度显示:百分比、速度、ETA
- ✨ 智能超时计算:根据文件大小自动计算
- ✨ 块大小优化:32KB → 128KB(4倍提升)
- ✨ 无超时限制:大文件传输不再超时
v3.1
- 守护进程长连接模式
- 服务器间直接传输
- 批量并发操作
- 统一配置管理
v3.0
- 基于 OpenSSH 配置
- 跳板机支持
- 元数据管理
🤝 贡献
欢迎提交 Issue 和 Pull Request!
📄 许可证
MIT License
👨💻 作者
Michael Zhang - @badseal
---
让远程服务器操作像本地一样简单高效! 🚀
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
为现有 SSH config 添加标准注释
读取现有的 SSH config 文件,为每个 Host 添加标准注释元数据
"""
import os
import re
from datetime import datetime
def parse_existing_config(config_path):
"""
解析现有配置,提取每个 Host 块
Returns:
List of (comments, host_line, config_lines) tuples
"""
if not os.path.exists(config_path):
return []
with open(config_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
hosts = []
current_comments = []
current_host_line = None
current_config = []
in_host_block = False
i = 0
while i < len(lines):
line = lines[i]
stripped = line.strip()
# 检查是否是 Host 行
if stripped.startswith('Host ') and not stripped.startswith('Host *'):
# 保存上一个 Host 块
if current_host_line:
hosts.append((current_comments, current_host_line, current_config))
# 开始新的 Host 块
current_host_line = line
current_config = []
in_host_block = True
# 保留之前收集的注释
elif in_host_block:
# 在 Host 块中
if stripped and not stripped.startswith('#'):
# 配置行(缩进的)
if line.startswith((' ', '\t')):
current_config.append(line)
else:
# 遇到非缩进的非注释行,Host 块结束
in_host_block = False
current_comments = []
if stripped.startswith('#'):
current_comments.append(line)
elif stripped.startswith('#'):
# Host 块中的注释(通常不应该有)
current_config.append(line)
elif not stripped:
# 空行,Host 块可能结束
current_config.append(line)
in_host_block = False
current_comments = []
else:
# 不在 Host 块中
if stripped.startswith('#') or not stripped:
current_comments.append(line)
else:
# 非注释非空行,清空注释缓存
current_comments = []
i += 1
# 保存最后一个 Host 块
if current_host_line:
hosts.append((current_comments, current_host_line, current_config))
return hosts
def extract_alias_from_host_line(host_line):
"""从 Host 行提取别名"""
match = re.match(r'Host\s+(.+)', host_line.strip())
if match:
return match.group(1).strip()
return None
def has_standard_comments(comments):
"""检查是否已有标准注释"""
comment_text = ''.join(comments)
return '# description:' in comment_text or '# environment:' in comment_text
def generate_standard_comments(alias):
"""生成标准注释"""
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
comments = [
f"\n# ===== {alias} =====\n",
f"# description: \n",
f"# environment: unknown\n",
f"# tags: \n",
f"# location: \n",
f"# created_at: {now}\n",
f"# updated_at: {now}\n"
]
return comments
def add_comments_to_config(config_path, output_path=None):
"""
为配置文件添加标准注释
Args:
config_path: 输入配置文件路径
output_path: 输出配置文件路径(如果为 None,则覆盖原文件)
"""
if output_path is None:
output_path = config_path
# 解析现有配置
hosts = parse_existing_config(config_path)
print(f"找到 {len(hosts)} 个 Host 配置")
# 生成新配置
new_lines = []
processed_count = 0
skipped_count = 0
for comments, host_line, config_lines in hosts:
alias = extract_alias_from_host_line(host_line)
if not alias:
# 无法提取别名,保持原样
new_lines.extend(comments)
new_lines.append(host_line)
new_lines.extend(config_lines)
skipped_count += 1
continue
# 检查是否已有标准注释
if has_standard_comments(comments):
# 已有标准注释,保持原样
new_lines.extend(comments)
new_lines.append(host_line)
new_lines.extend(config_lines)
skipped_count += 1
print(f" 跳过 {alias}(已有标准注释)")
else:
# 添加标准注释
standard_comments = generate_standard_comments(alias)
new_lines.extend(standard_comments)
new_lines.append(host_line)
new_lines.extend(config_lines)
processed_count += 1
print(f" 添加注释: {alias}")
# 写入新配置
with open(output_path, 'w', encoding='utf-8') as f:
f.writelines(new_lines)
print(f"\n完成:")
print(f" 处理: {processed_count} 个")
print(f" 跳过: {skipped_count} 个")
print(f" 输出: {output_path}")
if __name__ == '__main__':
config_path = os.path.expanduser('~/.ssh/config')
add_comments_to_config(config_path)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
部署公钥到远程服务器
将指定的公钥部署到远程服务器,实现从密码认证迁移到密钥认证。
"""
import sys
import os
import argparse
# 修复 Windows 终端 UTF-8 输出
if sys.platform == 'win32':
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
# 添加 lib 到路径
_script_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(_script_dir, 'lib'))
def deploy_pubkey(alias, pubkey_content, key_name):
"""
部署公钥到远程服务器
Args:
alias: 服务器别名
pubkey_content: 公钥内容
key_name: 密钥名称(用于标识)
Returns:
bool: 是否成功
"""
from config_v3 import SSHConfigLoaderV3
try:
# 加载配置
loader = SSHConfigLoaderV3()
params = loader.get_connection_params(alias)
# 检查是否有密码
if not params.get('password'):
print(f"错误: {alias} 没有配置密码,无法使用密码认证部署公钥")
return False
# 使用密码认证创建客户端
from paramiko_client import ParamikoClient
client = ParamikoClient(
host=params['hostname'],
user=params['user'],
port=params['port'],
password=params['password'],
timeout=30
)
print(f"正在连接到 {alias}...")
# 测试连接
result = client.execute("echo 'Connection OK'")
if not result.success:
print(f"错误: 无法连接到 {alias}")
return False
print("连接成功,开始部署公钥...")
# 创建 .ssh 目录(如果不存在)
result = client.execute("mkdir -p ~/.ssh && chmod 700 ~/.ssh")
if not result.success:
print(f"错误: 无法创建 .ssh 目录")
return False
# 检查公钥是否已存在
result = client.execute(f"grep -F '{pubkey_content.strip()}' ~/.ssh/authorized_keys 2>/dev/null")
if result.success and result.stdout.strip():
print(f"公钥已存在于 {alias},无需重复添加")
return True
# 追加公钥到 authorized_keys
escaped_pubkey = pubkey_content.strip().replace("'", "'\\''")
result = client.execute(
f"echo '{escaped_pubkey}' >> ~/.ssh/authorized_keys && "
f"chmod 600 ~/.ssh/authorized_keys"
)
if not result.success:
print(f"错误: 无法写入公钥到 authorized_keys")
print(f"错误信息: {result.stderr}")
return False
print(f"✓ 公钥已成功部署到 {alias}")
# 验证密钥认证是否工作
print("正在验证密钥认证...")
# 这里需要使用新的密钥文件测试连接
# 暂时跳过验证,由用户手动测试
return True
except Exception as e:
print(f"错误: {str(e)}")
return False
def main():
parser = argparse.ArgumentParser(description='部署公钥到远程服务器')
parser.add_argument('alias', help='服务器别名')
parser.add_argument('--pubkey-file', required=True, help='公钥文件路径')
parser.add_argument('--key-name', required=True, help='密钥名称(如 id_rsa_sa_legacy)')
args = parser.parse_args()
# 读取公钥内容
pubkey_file = os.path.expanduser(args.pubkey_file)
if not os.path.exists(pubkey_file):
print(f"错误: 公钥文件不存在: {pubkey_file}")
sys.exit(1)
with open(pubkey_file, 'r', encoding='utf-8') as f:
pubkey_content = f.read().strip()
if not pubkey_content:
print(f"错误: 公钥文件为空")
sys.exit(1)
# 部署公钥
success = deploy_pubkey(args.alias, pubkey_content, args.key_name)
if success:
print(f"\n成功!现在可以使用密钥 {args.key_name} 连接到 {args.alias}")
print(f"建议: 使用 migrate_to_key_auth.py 更新 SSH config")
sys.exit(0)
else:
print(f"\n失败: 无法部署公钥到 {args.alias}")
sys.exit(1)
if __name__ == '__main__':
main()
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
修复 SSH Config 文件
1. 从原始 JSON 配置中提取元数据填充注释
2. 统一证书文件路径格式为 ~/.ssh/keyfile
3. 在注释中添加密码字段(如果有)
"""
import os
import json
import re
from datetime import datetime
from pathlib import Path
def load_json_config(json_path):
"""加载 JSON 配置文件"""
try:
with open(json_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f" 警告: 无法加载 {json_path}: {e}")
return None
def find_json_config_by_alias(alias, json_dir):
"""根据别名查找对应的 JSON 配置文件"""
# 尝试直接匹配
json_path = os.path.join(json_dir, f"{alias}.json")
if os.path.exists(json_path):
return load_json_config(json_path)
# 尝试大小写不敏感匹配
for filename in os.listdir(json_dir):
if not filename.endswith('.json'):
continue
name_without_ext = os.path.splitext(filename)[0]
if name_without_ext.upper() == alias.upper():
json_path = os.path.join(json_dir, filename)
return load_json_config(json_path)
# 尝试从 JSON 中的 name 字段匹配
for filename in os.listdir(json_dir):
if not filename.endswith('.json'):
continue
json_path = os.path.join(json_dir, filename)
config = load_json_config(json_path)
if config and config.get('name') == alias:
return config
return None
def normalize_key_path(key_path):
"""
统一证书文件路径格式为 ~/.ssh/keyfile
Args:
key_path: 原始路径
Returns:
标准化后的路径
"""
if not key_path:
return key_path
# 已经是 ~/.ssh/ 格式,直接返回
if key_path.startswith('~/.ssh/'):
return key_path
# Windows 绝对路径转换为 ~/.ssh/ 格式
# C:\Users\zhangyang\.ssh\keyfile -> ~/.ssh/keyfile
# C:\Users\zhangyang/.ssh\keyfile -> ~/.ssh/keyfile
# C:/Users/zhangyang/.ssh/keyfile -> ~/.ssh/keyfile
# 统一路径分隔符
normalized = key_path.replace('\\', '/')
# 提取 .ssh 之后的部分
if '/.ssh/' in normalized:
parts = normalized.split('/.ssh/')
if len(parts) == 2:
return f"~/.ssh/{parts[1]}"
# 如果无法转换,返回原路径
return key_path
def extract_metadata_from_json(config):
"""从 JSON 配置中提取元数据"""
metadata = {
'description': '',
'environment': 'unknown',
'tags': [],
'location': '',
'password': ''
}
# 描述
if 'description' in config:
metadata['description'] = config['description']
elif 'notes' in config:
metadata['description'] = config['notes']
# 从 metadata 字段提取
if 'metadata' in config:
meta = config['metadata']
metadata['environment'] = meta.get('environment', 'unknown')
metadata['tags'] = meta.get('tags', [])
metadata['location'] = meta.get('location', '')
# 密码
if 'password' in config and config['password']:
metadata['password'] = config['password']
return metadata
def parse_ssh_config(config_path):
"""
解析 SSH config 文件
Returns:
List of blocks, each block is a dict with:
- comments: list of comment lines
- host_line: the Host line
- config_lines: list of config lines
- alias: extracted alias
"""
if not os.path.exists(config_path):
return []
with open(config_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
blocks = []
current_comments = []
current_host_line = None
current_config = []
in_host_block = False
i = 0
while i < len(lines):
line = lines[i]
stripped = line.strip()
# 检查是否是 Host 行
if stripped.startswith('Host ') and not stripped.startswith('Host *'):
# 保存上一个 Host 块
if current_host_line:
alias = extract_alias_from_host_line(current_host_line)
blocks.append({
'comments': current_comments,
'host_line': current_host_line,
'config_lines': current_config,
'alias': alias
})
# 开始新的 Host 块
current_host_line = line
current_config = []
in_host_block = True
elif in_host_block:
# 在 Host 块中
if stripped and not stripped.startswith('#'):
# 配置行(缩进的)
if line.startswith((' ', '\t')):
current_config.append(line)
else:
# 遇到非缩进的非注释行,Host 块结束
in_host_block = False
current_comments = []
if stripped.startswith('#'):
current_comments.append(line)
elif stripped.startswith('#'):
current_config.append(line)
elif not stripped:
current_config.append(line)
in_host_block = False
current_comments = []
else:
# 不在 Host 块中
if stripped.startswith('#') or not stripped:
current_comments.append(line)
else:
current_comments = []
i += 1
# 保存最后一个 Host 块
if current_host_line:
alias = extract_alias_from_host_line(current_host_line)
blocks.append({
'comments': current_comments,
'host_line': current_host_line,
'config_lines': current_config,
'alias': alias
})
return blocks
def extract_alias_from_host_line(host_line):
"""从 Host 行提取别名"""
match = re.match(r'Host\s+(.+)', host_line.strip())
if match:
return match.group(1).strip()
return None
def generate_updated_comments(alias, metadata):
"""生成更新后的注释"""
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
comments = [
f"\n# ===== {alias} =====\n",
f"# description: {metadata.get('description', '')}\n",
f"# environment: {metadata.get('environment', 'unknown')}\n",
]
# 标签
tags = metadata.get('tags', [])
if tags:
comments.append(f"# tags: {','.join(tags)}\n")
else:
comments.append(f"# tags: \n")
# 位置
location = metadata.get('location', '')
comments.append(f"# location: {location}\n")
# 密码(如果有)
password = metadata.get('password', '')
if password:
comments.append(f"# password: {password}\n")
# 时间
comments.append(f"# created_at: {now}\n")
comments.append(f"# updated_at: {now}\n")
return comments
def normalize_config_lines(config_lines):
"""标准化配置行中的证书路径"""
normalized = []
for line in config_lines:
# 检查是否是 IdentityFile 行
if 'IdentityFile' in line:
match = re.match(r'(\s*)IdentityFile\s+(.+)', line)
if match:
indent = match.group(1)
key_path = match.group(2).strip()
normalized_path = normalize_key_path(key_path)
normalized.append(f"{indent}IdentityFile {normalized_path}\n")
continue
normalized.append(line)
return normalized
def fix_ssh_config(config_path, json_dir, output_path=None):
"""
修复 SSH config 文件
Args:
config_path: SSH config 文件路径
json_dir: JSON 配置目录
output_path: 输出文件路径(如果为 None,则覆盖原文件)
"""
if output_path is None:
output_path = config_path
# 解析现有配置
blocks = parse_ssh_config(config_path)
print(f"找到 {len(blocks)} 个 Host 配置")
# 处理每个块
new_lines = []
updated_count = 0
normalized_count = 0
password_count = 0
for block in blocks:
alias = block['alias']
if not alias:
# 无法提取别名,保持原样
new_lines.extend(block['comments'])
new_lines.append(block['host_line'])
new_lines.extend(block['config_lines'])
continue
# 查找对应的 JSON 配置
json_config = find_json_config_by_alias(alias, json_dir)
if json_config:
# 提取元数据
metadata = extract_metadata_from_json(json_config)
# 生成新注释
new_comments = generate_updated_comments(alias, metadata)
new_lines.extend(new_comments)
updated_count += 1
if metadata.get('password'):
password_count += 1
print(f" 更新 {alias}(包含密码)")
else:
print(f" 更新 {alias}")
else:
# 没有找到 JSON 配置,保持原注释
new_lines.extend(block['comments'])
print(f" 跳过 {alias}(未找到 JSON 配置)")
# Host 行
new_lines.append(block['host_line'])
# 标准化配置行中的证书路径
normalized_config = normalize_config_lines(block['config_lines'])
# 检查是否有路径被标准化
if normalized_config != block['config_lines']:
normalized_count += 1
new_lines.extend(normalized_config)
# 写入新配置
with open(output_path, 'w', encoding='utf-8') as f:
f.writelines(new_lines)
print(f"\n完成:")
print(f" 更新元数据: {updated_count} 个")
print(f" 标准化路径: {normalized_count} 个")
print(f" 添加密码: {password_count} 个")
print(f" 输出: {output_path}")
if __name__ == '__main__':
config_path = os.path.expanduser('~/.ssh/config')
json_dir = os.path.expanduser('~/.ssh/server_config')
fix_ssh_config(config_path, json_dir)
#!/bin/bash
# 获取系统信息脚本
# 操作系统
if [ -f /etc/os-release ]; then
OS=$(grep PRETTY_NAME /etc/os-release | cut -d'"' -f2)
else
OS=$(uname -s)
fi
# CPU核心数
CPU=$(nproc 2>/dev/null || grep -c processor /proc/cpuinfo 2>/dev/null || echo "?")
# 内存(GB)
if [ -f /proc/meminfo ]; then
MEM_KB=$(grep MemTotal /proc/meminfo | grep -o '[0-9]*')
MEM_GB=$(echo "scale=1; $MEM_KB/1024/1024" | bc 2>/dev/null || echo "?")
MEM="${MEM_GB}G"
else
MEM="?"
fi
# 磁盘总空间
DISK=$(df -h / | tail -1 | tr -s ' ' | cut -d' ' -f2)
# 输出格式化信息
echo "$OS/$CPU核/${MEM}内存/${DISK}磁盘"
"""
SSH Skill - 系统级SSH连接管理工具
基于系统OpenSSH的SSH客户端,提供稳定可靠的SSH连接能力。
核心特性:
- ControlMaster 连接复用(10-100x性能提升)
- ProxyJump 跳板机支持(支持多级)
- 项目级配置管理
- 流式输出和交互式会话
- 批量并发操作
快速开始:
from ssh_skill import SSHConfigLoader
# 加载配置
client = SSHConfigLoader.from_file(".ssh_config/prod.json")
# 执行命令
result = client.execute("whoami && hostname")
print(result.stdout)
# 批量操作
from ssh_skill import SSHCluster
cluster = SSHCluster.from_directory(".ssh_config/cluster/")
results = cluster.execute_all("uptime", parallel=True)
"""
from .client import SSHClient, SSHResult
from .config import SSHConfigLoader, ServerConfig
from .cluster import SSHCluster, SSHBatchOperations
from .utils import (
check_ssh_available,
get_ssh_version,
validate_key_file
)
__version__ = "0.1.0"
__all__ = [
# 核心类
"SSHClient",
"SSHResult",
"SSHConfigLoader",
"ServerConfig",
"SSHCluster",
"SSHBatchOperations",
# 工具函数
"check_ssh_available",
"get_ssh_version",
"validate_key_file",
]
"""
批量服务器操作模块 v3.1
从 SSH config 读取服务器列表,提供多服务器并发操作能力。
支持智能客户端选择(密钥认证用原生 SSH,密码认证用 Paramiko)。
"""
from typing import List, Dict, Optional, Union
from concurrent.futures import ThreadPoolExecutor, as_completed
try:
from .config_v3 import SSHConfigLoaderV3
from .native_ssh_client import SSHResult
except ImportError:
from config_v3 import SSHConfigLoaderV3
from native_ssh_client import SSHResult
class SSHCluster:
"""SSH集群管理类 v3.1,从 SSH config 读取服务器列表,智能选择客户端类型"""
def __init__(self, clients: Dict[str, object], max_workers: int = 10):
"""
初始化集群
Args:
clients: {alias: NativeSSHClient 或 ParamikoClient}
max_workers: 最大并发数
"""
self.clients = clients
self.max_workers = max_workers
@classmethod
def from_ssh_config(cls, aliases: List[str] = None,
environment: str = None,
tags: List[str] = None,
max_workers: int = 10) -> 'SSHCluster':
"""
从 SSH config 创建集群(智能选择客户端类型)
Args:
aliases: 指定别名列表(可选)
environment: 按环境过滤(可选)
tags: 按标签过滤(可选)
max_workers: 最大并发数
"""
loader = SSHConfigLoaderV3()
if aliases:
# 使用指定的别名列表
host_list = aliases
else:
# 从 SSH config 获取所有 Host
host_list = cls._list_all_hosts(loader)
# 创建客户端(使用智能选择)
clients = {}
for alias in host_list:
try:
params = loader.get_connection_params(alias)
metadata = params.get('metadata', {})
# 按环境过滤
if environment and metadata.get('environment', '') != environment:
continue
# 按标签过滤
if tags:
host_tags = metadata.get('tags', [])
if not any(t in host_tags for t in tags):
continue
# 使用智能选择创建客户端
client = loader.from_alias(alias)
clients[alias] = client
except Exception:
continue # 跳过无法加载的配置
return cls(clients, max_workers)
@staticmethod
def _list_all_hosts(loader: SSHConfigLoaderV3) -> List[str]:
"""从 SSH config 列出所有 Host 别名"""
import os
import re
config_path = loader.config_path
if not os.path.exists(config_path):
return []
hosts = []
with open(config_path, 'r', encoding='utf-8') as f:
for line in f:
stripped = line.strip()
if stripped.startswith('Host ') and not stripped.startswith('Host *'):
match = re.match(r'Host\s+(.+)', stripped)
if match:
alias = match.group(1).strip()
# 跳过通配符
if '*' not in alias and '?' not in alias:
hosts.append(alias)
return hosts
def execute_all(self, command: str, parallel: bool = True,
timeout: Optional[int] = None) -> Dict[str, SSHResult]:
"""在所有服务器上执行命令"""
if parallel:
return self._execute_parallel(command, timeout)
else:
return self._execute_serial(command, timeout)
def _execute_serial(self, command: str, timeout: Optional[int]) -> Dict[str, SSHResult]:
"""串行执行命令"""
results = {}
for alias, client in self.clients.items():
try:
if timeout:
original_timeout = client.timeout
client.timeout = timeout
result = client.execute(command)
client.timeout = original_timeout
else:
result = client.execute(command)
results[alias] = result
except Exception as e:
results[alias] = SSHResult(
success=False, stdout="",
stderr=f"执行异常: {str(e)}", exit_code=-1
)
return results
def _execute_parallel(self, command: str, timeout: Optional[int]) -> Dict[str, SSHResult]:
"""并发执行命令"""
results = {}
def execute_on_client(alias, client):
try:
if timeout:
original_timeout = client.timeout
client.timeout = timeout
result = client.execute(command)
client.timeout = original_timeout
else:
result = client.execute(command)
return alias, result
except Exception as e:
return alias, SSHResult(
success=False, stdout="",
stderr=f"执行异常: {str(e)}", exit_code=-1
)
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
futures = {
executor.submit(execute_on_client, alias, client): alias
for alias, client in self.clients.items()
}
for future in as_completed(futures):
alias, result = future.result()
results[alias] = result
return results
def health_check_all(self, check_command: str = "echo 'OK'",
parallel: bool = True,
timeout: Optional[int] = None) -> Dict[str, bool]:
"""批量健康检查"""
results = self.execute_all(check_command, parallel=parallel, timeout=timeout)
return {name: result.success for name, result in results.items()}
def upload_all(self, local_path: str, remote_path: str,
parallel: bool = True) -> Dict[str, SSHResult]:
"""批量上传文件"""
def upload_to_client(alias, client):
try:
result = client.upload(local_path, remote_path)
return alias, result
except Exception as e:
return alias, SSHResult(
success=False, stdout="",
stderr=f"上传异常: {str(e)}", exit_code=-1
)
results = {}
if parallel:
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
futures = {
executor.submit(upload_to_client, alias, client): alias
for alias, client in self.clients.items()
}
for future in as_completed(futures):
alias, result = future.result()
results[alias] = result
else:
for alias, client in self.clients.items():
_, result = upload_to_client(alias, client)
results[alias] = result
return results
"""
配置管理模块 v3.0
基于标准 OpenSSH config 格式的配置加载器
新特性:
1. 从 ~/.ssh/config 加载配置
2. 支持别名(Host)
3. 元数据从注释中解析
4. 完全兼容 ProxyJump(跳板机)
5. 支持密码认证(从注释中读取)
"""
import os
import json
import re
from typing import Dict, Optional, List
from pathlib import Path
try:
import paramiko
except ImportError:
raise ImportError("需要安装 paramiko 库: pip install paramiko")
class SSHConfigLoaderV3:
"""SSH Config 加载器 v3.0
从标准 OpenSSH config 文件加载配置
"""
def __init__(self, config_path: Optional[str] = None,
metadata_path: Optional[str] = None):
"""
初始化加载器
Args:
config_path: SSH config 文件路径,默认 ~/.ssh/config
metadata_path: 元数据文件路径,默认 ~/.ssh/config_metadata.json
"""
if config_path is None:
config_path = os.path.expanduser("~/.ssh/config")
if metadata_path is None:
metadata_path = os.path.expanduser("~/.ssh/config_metadata.json")
self.config_path = config_path
self.metadata_path = metadata_path
def load_ssh_config(self, alias: str) -> dict:
"""
从 SSH config 加载指定别名的配置
Args:
alias: 主机别名
Returns:
配置字典
Raises:
FileNotFoundError: 配置文件不存在
ValueError: 别名不存在
"""
if not os.path.exists(self.config_path):
raise FileNotFoundError(f"SSH config 文件不存在: {self.config_path}")
# 解析 SSH config
ssh_config = paramiko.SSHConfig()
with open(self.config_path, 'r', encoding='utf-8') as f:
ssh_config.parse(f)
# 获取配置
try:
host_config = ssh_config.lookup(alias)
except Exception as e:
raise ValueError(f"无法解析别名 '{alias}': {e}")
# 检查是否真的找到了配置(paramiko 会返回默认值)
if host_config.get('hostname') == alias and not self._alias_exists(alias):
raise ValueError(f"别名 '{alias}' 不存在于 SSH config 中")
return host_config
def _alias_exists(self, alias: str) -> bool:
"""检查别名是否存在于 SSH config 中"""
try:
with open(self.config_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line.startswith('Host ') and not line.startswith('Host *'):
# 提取 Host 名称
import re
host_match = re.match(r'Host\s+(.+)', line)
if host_match and host_match.group(1).strip() == alias:
return True
return False
except Exception:
return False
def load_metadata(self, alias: str) -> dict:
"""
从注释中加载元数据
Args:
alias: 主机别名
Returns:
元数据字典(包括密码)
"""
metadata = {
'description': '',
'environment': 'unknown',
'tags': [],
'location': '',
'password': ''
}
# 读取 config 文件,查找该 Host 前的注释
if not os.path.exists(self.config_path):
return metadata
with open(self.config_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
# 查找 Host 行
host_line_index = -1
for i, line in enumerate(lines):
stripped = line.strip()
if stripped.startswith('Host ') and not stripped.startswith('Host *'):
import re
match = re.match(r'Host\s+(.+)', stripped)
if match and match.group(1).strip() == alias:
host_line_index = i
break
if host_line_index == -1:
return metadata
# 向前查找注释
comment_lines = []
i = host_line_index - 1
while i >= 0:
line = lines[i]
stripped = line.strip()
if stripped.startswith('#') or not stripped:
comment_lines.insert(0, line)
i -= 1
else:
break
# 解析注释
for line in comment_lines:
line = line.strip()
if not line.startswith('#'):
continue
# 移除开头的 #
line = line[1:].strip()
# 跳过分隔线
if line.startswith('=====') or line == '':
continue
# 解析 key: value 格式
if ':' in line:
key, value = line.split(':', 1)
key = key.strip()
value = value.strip()
if key == 'description':
metadata['description'] = value
elif key == 'environment':
metadata['environment'] = value
elif key == 'tags':
metadata['tags'] = [t.strip() for t in value.split(',') if t.strip()]
elif key == 'location':
metadata['location'] = value
elif key == 'password':
metadata['password'] = value
return metadata
def get_connection_params(self, alias: str) -> dict:
"""
获取连接参数(用于创建 SSH 客户端)
Args:
alias: 主机别名
Returns:
连接参数字典(包括密码)
"""
config = self.load_ssh_config(alias)
metadata = self.load_metadata(alias)
# 提取连接参数
params = {
'hostname': config.get('hostname'),
'user': config.get('user'),
'port': int(config.get('port', 22)),
'timeout': 30, # 默认超时
}
# 密钥文件
identity_files = config.get('identityfile')
if identity_files:
# paramiko 返回的是列表
if isinstance(identity_files, list):
params['key_file'] = identity_files[0]
else:
params['key_file'] = identity_files
# 密码(从注释元数据中获取)
if metadata.get('password'):
params['password'] = metadata['password']
# ProxyJump(跳板机)
proxy_jump = config.get('proxyjump')
if proxy_jump:
params['proxy_jump'] = proxy_jump
# ForwardAgent(SSH agent 转发)
forward_agent = config.get('forwardagent', 'no').lower()
params['forward_agent'] = forward_agent in ('yes', 'true', '1')
# 元数据
params['metadata'] = metadata
params['alias'] = alias
return params
def from_alias(self, alias: str):
"""
通过别名创建 SSH 客户端(智能选择)
策略:
- 有密钥文件且无密码 → 使用 NativeSSHClient(原生 SSH)
- 有密码 → 使用 ParamikoClient(Paramiko)
Args:
alias: 主机别名
Returns:
NativeSSHClient 或 ParamikoClient 实例
"""
params = self.get_connection_params(alias)
has_key = params.get('key_file') is not None
has_password = params.get('password') is not None
# 智能选择客户端类型
if has_key and not has_password:
# 密钥认证 → 使用原生 SSH(支持 agent forwarding)
try:
from .native_ssh_client import NativeSSHClient
except ImportError:
from native_ssh_client import NativeSSHClient
client = NativeSSHClient(
host=params['hostname'],
user=params['user'],
port=params['port'],
key_file=params.get('key_file'),
timeout=params['timeout'],
proxy_jump=params.get('proxy_jump'),
forward_agent=params.get('forward_agent', False),
alias=alias
)
else:
# 密码认证 → 使用 Paramiko
try:
from .paramiko_client import ParamikoClient
except ImportError:
from paramiko_client import ParamikoClient
client = ParamikoClient(
host=params['hostname'],
user=params['user'],
port=params['port'],
password=params.get('password'),
key_file=params.get('key_file'),
timeout=params['timeout']
)
# 设置别名(用于守护进程标识)
client.alias = alias
return client
@staticmethod
def get_default_config_path() -> str:
"""获取默认 SSH config 路径"""
return os.path.expanduser("~/.ssh/config")
@staticmethod
def get_default_metadata_path() -> str:
"""获取默认元数据路径"""
return os.path.expanduser("~/.ssh/config_metadata.json")
# 向后兼容:提供全局函数接口
def get_config_loader_v3(config_path: Optional[str] = None,
metadata_path: Optional[str] = None) -> SSHConfigLoaderV3:
"""
获取配置加载器实例
Args:
config_path: SSH config 文件路径
metadata_path: 元数据文件路径
Returns:
SSHConfigLoaderV3 实例
"""
return SSHConfigLoaderV3(config_path, metadata_path)
"""
原生 SSH 客户端模块
使用原生 ssh/scp 命令,充分利用系统级 SSH 特性:
- ControlMaster 自动连接复用
- ProxyJump 原生跳板机支持
- ForwardAgent 完美支持
- 性能更好,功能更完整
适用场景:密钥认证(无密码)
"""
import subprocess
import os
import tempfile
from typing import Optional, Iterator
from dataclasses import dataclass
@dataclass
class SSHResult:
"""SSH命令执行结果"""
success: bool
stdout: str
stderr: str
exit_code: int
class NativeSSHClient:
"""基于原生 SSH 命令的客户端
使用系统原生 ssh/scp 命令,支持:
- 密钥认证
- ProxyJump 跳板机
- ForwardAgent 代理转发
- ControlMaster 连接复用
"""
def __init__(
self,
host: str,
user: str,
port: int = 22,
key_file: Optional[str] = None,
timeout: int = 30,
proxy_jump: Optional[str] = None,
forward_agent: bool = False,
alias: Optional[str] = None
):
"""
初始化原生 SSH 客户端
Args:
host: SSH服务器地址
user: SSH用户名
port: SSH端口,默认22
key_file: SSH私钥文件路径
timeout: 连接超时时间(秒),默认30
proxy_jump: ProxyJump 配置(如 "user@jumphost:port")
forward_agent: 是否启用 SSH agent forwarding
alias: 服务器别名(用于 ControlMaster)
"""
self.host = host
self.user = user
self.port = port
self.key_file = key_file
self.timeout = timeout
self.proxy_jump = proxy_jump
self.forward_agent = forward_agent
self.alias = alias or f"{user}@{host}:{port}"
def _build_ssh_base_args(self) -> list:
"""构建 SSH 基础参数"""
args = [
"ssh",
"-p", str(self.port),
"-o", "StrictHostKeyChecking=no",
"-o", "UserKnownHostsFile=/dev/null",
"-o", f"ConnectTimeout={self.timeout}",
]
# 密钥文件
if self.key_file:
args.extend(["-i", os.path.expanduser(self.key_file)])
# ProxyJump
if self.proxy_jump:
args.extend(["-o", f"ProxyJump={self.proxy_jump}"])
# ForwardAgent
if self.forward_agent:
args.extend(["-o", "ForwardAgent=yes"])
# ControlMaster(连接复用)
# 注意:Windows + 跳板机场景下 ControlMaster 可能不稳定,暂时禁用
# TODO: 后续优化 Windows 上的 ControlMaster 支持
# control_path = self._get_control_path()
# args.extend([
# "-o", "ControlMaster=auto",
# "-o", f"ControlPath={control_path}",
# "-o", "ControlPersist=600", # 保持10分钟
# ])
return args
def _get_control_path(self) -> str:
"""获取 ControlMaster socket 路径"""
# 使用临时目录
temp_dir = tempfile.gettempdir()
# 使用别名作为标识,避免路径过长
safe_alias = self.alias.replace('/', '_').replace('@', '_').replace(':', '_')
return os.path.join(temp_dir, f"ssh-control-{safe_alias}")
def execute(self, command: str) -> SSHResult:
"""
执行SSH命令
Args:
command: 要执行的命令
Returns:
SSHResult对象,包含执行结果
"""
try:
args = self._build_ssh_base_args()
args.append(f"{self.user}@{self.host}")
args.append(command)
result = subprocess.run(
args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding='utf-8',
errors='replace',
timeout=self.timeout
)
return SSHResult(
success=(result.returncode == 0),
stdout=result.stdout,
stderr=result.stderr,
exit_code=result.returncode
)
except subprocess.TimeoutExpired:
return SSHResult(
success=False,
stdout="",
stderr=f"Command timeout after {self.timeout} seconds",
exit_code=-1
)
except Exception as e:
return SSHResult(
success=False,
stdout="",
stderr=f"Execution error: {str(e)}",
exit_code=-1
)
def upload(self, local_path: str, remote_path: str, timeout: Optional[int] = None, show_progress: bool = True) -> SSHResult:
"""
上传文件到远程服务器
Args:
local_path: 本地文件路径
remote_path: 远程文件路径
timeout: 超时时间(秒),None 表示根据文件大小自动计算
show_progress: 是否显示传输进度
Returns:
SSHResult对象,包含操作结果
"""
# 检查本地文件是否存在
if not os.path.exists(local_path):
return SSHResult(
success=False,
stdout="",
stderr=f"Local file not found: {local_path}",
exit_code=-1
)
try:
# 根据文件大小自动计算超时时间
if timeout is None:
file_size_mb = os.path.getsize(local_path) / (1024 * 1024)
# 假设最低速度 1MB/s,加上 60 秒缓冲时间
calculated_timeout = int(file_size_mb / 1.0) + 60
# 最小 60 秒,最大 3600 秒(1小时)
actual_timeout = max(60, min(calculated_timeout, 3600))
else:
actual_timeout = timeout
args = ["scp"]
# 基本参数
args.extend(["-P", str(self.port)])
args.extend(["-o", "StrictHostKeyChecking=no"])
args.extend(["-o", "UserKnownHostsFile=/dev/null"])
# 密钥文件
if self.key_file:
args.extend(["-i", os.path.expanduser(self.key_file)])
# ProxyJump
if self.proxy_jump:
args.extend(["-o", f"ProxyJump={self.proxy_jump}"])
# ControlMaster(复用 SSH 连接)
# 注意:Windows + 跳板机场景下 ControlMaster 可能不稳定,暂时禁用
# control_path = self._get_control_path()
# args.extend([
# "-o", "ControlMaster=auto",
# "-o", f"ControlPath={control_path}",
# "-o", "ControlPersist=600",
# ])
# 源和目标
args.append(local_path)
args.append(f"{self.user}@{self.host}:{remote_path}")
result = subprocess.run(
args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding='utf-8',
errors='replace',
timeout=actual_timeout
)
return SSHResult(
success=(result.returncode == 0),
stdout=f"File uploaded: {local_path} -> {remote_path}" if result.returncode == 0 else result.stdout,
stderr=result.stderr if result.returncode != 0 else "",
exit_code=result.returncode
)
except subprocess.TimeoutExpired:
return SSHResult(
success=False,
stdout="",
stderr=f"Upload timeout after {timeout or self.timeout} seconds",
exit_code=-1
)
except Exception as e:
return SSHResult(
success=False,
stdout="",
stderr=f"Upload error: {str(e)}",
exit_code=-1
)
def download(self, remote_path: str, local_path: str, timeout: Optional[int] = None, show_progress: bool = True) -> SSHResult:
"""
从远程服务器下载文件
Args:
remote_path: 远程文件路径
local_path: 本地文件路径
timeout: 超时时间(秒),None 表示根据文件大小自动计算
show_progress: 是否显示传输进度
Returns:
SSHResult对象,包含操作结果
"""
try:
# 确保本地目录存在
local_dir = os.path.dirname(local_path)
if local_dir and not os.path.exists(local_dir):
os.makedirs(local_dir, exist_ok=True)
# 如果没有指定超时,使用默认值(下载时无法提前知道文件大小)
actual_timeout = timeout if timeout is not None else 600 # 默认 10 分钟
args = ["scp"]
# 基本参数
args.extend(["-P", str(self.port)])
args.extend(["-o", "StrictHostKeyChecking=no"])
args.extend(["-o", "UserKnownHostsFile=/dev/null"])
# 密钥文件
if self.key_file:
args.extend(["-i", os.path.expanduser(self.key_file)])
# ProxyJump
if self.proxy_jump:
args.extend(["-o", f"ProxyJump={self.proxy_jump}"])
# ControlMaster(复用 SSH 连接)
# 注意:Windows + 跳板机场景下 ControlMaster 可能不稳定,暂时禁用
# control_path = self._get_control_path()
# args.extend([
# "-o", "ControlMaster=auto",
# "-o", f"ControlPath={control_path}",
# "-o", "ControlPersist=600",
# ])
# 源和目标
args.append(f"{self.user}@{self.host}:{remote_path}")
args.append(local_path)
result = subprocess.run(
args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding='utf-8',
errors='replace',
timeout=actual_timeout
)
return SSHResult(
success=(result.returncode == 0),
stdout=f"File downloaded: {remote_path} -> {local_path}" if result.returncode == 0 else result.stdout,
stderr=result.stderr if result.returncode != 0 else "",
exit_code=result.returncode
)
except subprocess.TimeoutExpired:
return SSHResult(
success=False,
stdout="",
stderr=f"Download timeout after {timeout or self.timeout} seconds",
exit_code=-1
)
except Exception as e:
return SSHResult(
success=False,
stdout="",
stderr=f"Download error: {str(e)}",
exit_code=-1
)
def test_connection(self) -> SSHResult:
"""
测试SSH连接
Returns:
SSHResult对象,包含测试结果
"""
return self.execute("echo 'Connection OK'")
def execute_stream(self, command: str, timeout: Optional[int] = None) -> Iterator[str]:
"""
实时流式执行命令,逐行返回输出
Args:
command: 要执行的命令
timeout: 总超时时间(秒),默认使用实例的timeout
Yields:
命令输出的每一行
"""
try:
args = self._build_ssh_base_args()
args.append(f"{self.user}@{self.host}")
args.append(command)
process = subprocess.Popen(
args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding='utf-8',
errors='replace'
)
# 逐行读取输出
if process.stdout:
for line in iter(process.stdout.readline, ''):
if not line:
break
yield line.rstrip('\n')
# 等待进程结束
process.wait(timeout=timeout or self.timeout)
# 如果有错误输出,也返回
if process.stderr:
for line in process.stderr:
yield f"[STDERR] {line.rstrip('\n')}"
except subprocess.TimeoutExpired:
if process:
process.kill()
yield f"[ERROR] Command timeout after {timeout or self.timeout} seconds"
except Exception as e:
yield f"[ERROR] Execution error: {str(e)}"
"""
原生 SSH 降级模块
当检测到复杂场景(ProxyCommand、passphrase 等)时,
降级使用原生 ssh 命令而非 Paramiko。
"""
import subprocess
import os
import json
from typing import Optional, Dict, Tuple
def _get_windows_native_ssh_path(exe_name: str = 'ssh.exe') -> Optional[str]:
"""
获取 Windows 原生 OpenSSH 可执行文件的完整路径
通过 %SystemRoot% 环境变量动态定位 System32\\OpenSSH 目录,
而非依赖 PATH 查找。这样可以避免 Git Bash 的 ssh.exe 优先级
高于 Windows 原生版本的问题。
Git 的 ssh.exe 无法访问 Windows SSH Agent 服务,必须使用原生版本。
Args:
exe_name: 可执行文件名,如 'ssh.exe'、'ssh-add.exe'
Returns:
完整路径字符串,不存在则返回 None
"""
if os.name != 'nt':
return None
system_root = os.environ.get('SystemRoot', r'C:\Windows')
exe_path = os.path.join(system_root, 'System32', 'OpenSSH', exe_name)
if os.path.isfile(exe_path):
return exe_path
return None
def check_windows_ssh_availability() -> Tuple[bool, str]:
"""
检查 Windows 原生 OpenSSH 客户端是否可用
Returns:
(is_available, message_or_path) 元组
- 可用时:(True, ssh.exe 完整路径)
- 不可用时:(False, 错误信息)
"""
if os.name != 'nt':
return False, "非 Windows 系统"
ssh_path = _get_windows_native_ssh_path('ssh.exe')
if not ssh_path:
return False, "未安装 Windows 原生 OpenSSH 客户端(System32\\OpenSSH\\ssh.exe 不存在)"
return True, ssh_path
def should_use_native_ssh(ssh_config: dict, metadata: dict = None) -> Tuple[bool, str]:
"""
检测是否应该使用原生 SSH 而非 Paramiko
Args:
ssh_config: SSH 配置字典(从 paramiko.SSHConfig.lookup 获取)
metadata: 元数据字典(可选)
Returns:
(should_fallback, reason) 元组
"""
reasons = []
# 检测 ProxyCommand(包括 Cloudflare Tunnel)
proxy_command = ssh_config.get('proxycommand')
if proxy_command:
# Cloudflare Tunnel
if 'cloudflared' in proxy_command.lower():
reasons.append("检测到 Cloudflare Tunnel (ProxyCommand)")
# 其他 ProxyCommand
else:
reasons.append(f"检测到 ProxyCommand: {proxy_command}")
# 检测 ProxyJump(多级跳板机)
proxy_jump = ssh_config.get('proxyjump')
if proxy_jump and ',' in proxy_jump:
# 多级跳板机(单级跳板机 Paramiko 可以处理)
reasons.append(f"检测到多级跳板机: {proxy_jump}")
# 检测密钥文件是否需要 passphrase
identity_file = ssh_config.get('identityfile')
if identity_file:
# 如果是列表,取第一个
if isinstance(identity_file, list):
identity_file = identity_file[0] if identity_file else None
if identity_file and _key_has_passphrase(identity_file):
reasons.append("检测到密钥需要 passphrase(建议使用 ssh-agent)")
# 检测其他复杂配置
if ssh_config.get('localforward') or ssh_config.get('remoteforward'):
reasons.append("检测到端口转发配置")
if ssh_config.get('dynamicforward'):
reasons.append("检测到动态端口转发(SOCKS 代理)")
# 如果有任何复杂场景,建议降级
if reasons:
return True, "; ".join(reasons)
return False, ""
def _key_has_passphrase(key_file: str) -> bool:
"""
检测密钥文件是否有 passphrase 保护
注意:这是一个启发式检测,不是 100% 准确
"""
try:
key_file = os.path.expanduser(key_file)
if not os.path.exists(key_file):
return False
with open(key_file, 'r') as f:
content = f.read()
# 检测加密标记(旧格式)
if 'ENCRYPTED' in content:
return True
# OpenSSH 新格式的加密密钥
if 'BEGIN OPENSSH PRIVATE KEY' in content:
# 提取所有 base64 行(排除 BEGIN/END 行)
lines = content.strip().split('\n')
base64_lines = [line for line in lines
if line and not line.startswith('-----')]
if base64_lines:
try:
import base64
# 合并所有 base64 行后解码
base64_content = ''.join(base64_lines)
decoded = base64.b64decode(base64_content).decode('latin-1', errors='ignore')
# 检查是否包含加密算法标记
# 如果包含 'none' 且没有其他加密算法,表示未加密
has_encryption = any(marker in decoded for marker in
['aes128-ctr', 'aes192-ctr', 'aes256-ctr',
'aes128-cbc', 'aes192-cbc', 'aes256-cbc'])
if has_encryption:
return True
# 如果只有 'none',表示未加密
if 'none' in decoded and not has_encryption:
return False
except Exception:
pass
return False
except Exception:
return False
def execute_native_ssh(
alias: str,
command: str,
timeout: int = 120,
ssh_config_path: Optional[str] = None
) -> Dict:
"""
使用原生 ssh 命令执行远程命令
Windows 平台:通过 PowerShell 执行 SSH(以访问 Windows SSH Agent)
Unix/Linux:直接执行 SSH
Args:
alias: SSH 别名
command: 要执行的命令
timeout: 超时时间(秒)
ssh_config_path: SSH 配置文件路径(默认 ~/.ssh/config)
Returns:
结果字典 {success, exit_code, stdout, stderr}
"""
if ssh_config_path is None:
ssh_config_path = os.path.expanduser("~/.ssh/config")
# Windows 平台:通过 PowerShell 执行 SSH(才能访问 Windows SSH Agent)
if os.name == 'nt':
# 检查 OpenSSH 是否可用
ssh_available, ssh_msg = check_windows_ssh_availability()
if not ssh_available:
return {
'success': False,
'exit_code': -1,
'stdout': '',
'stderr': f'Windows OpenSSH 不可用: {ssh_msg}\n\n' +
'启用方法(管理员 PowerShell):\n' +
'Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0\n\n' +
'或通过设置界面:\n' +
'设置 → 应用 → 可选功能 → 添加功能 → OpenSSH 客户端',
'method': 'native_ssh_windows'
}
# 使用原生 SSH 路径(而非 PATH 中优先级更高的 Git SSH)
native_ssh_exe = ssh_msg # check_windows_ssh_availability 成功时返回完整路径
# 转换路径为 Windows 格式
ssh_config_path_win = ssh_config_path.replace('/', '\\')
# 构建 PowerShell SSH 命令
# 使用 & "path" 语法指定原生 SSH,-NoProfile 加快启动
ssh_cmd = [
'powershell',
'-NoProfile',
'-Command',
f'& "{native_ssh_exe}" -F "{ssh_config_path_win}" -o BatchMode=yes -o StrictHostKeyChecking=accept-new {alias} "{command}"'
]
else:
# Unix/Linux:直接执行 SSH
ssh_cmd = [
'ssh',
'-F', ssh_config_path,
'-o', 'BatchMode=yes',
'-o', 'StrictHostKeyChecking=accept-new',
alias,
command
]
try:
result = subprocess.run(
ssh_cmd,
capture_output=True,
text=True,
timeout=timeout,
encoding='utf-8',
errors='replace'
)
return {
'success': result.returncode == 0,
'exit_code': result.returncode,
'stdout': result.stdout,
'stderr': result.stderr,
'method': 'native_ssh_windows' if os.name == 'nt' else 'native_ssh'
}
except subprocess.TimeoutExpired:
return {
'success': False,
'exit_code': -1,
'stdout': '',
'stderr': f'命令执行超时({timeout}秒)',
'method': 'native_ssh_windows' if os.name == 'nt' else 'native_ssh'
}
except Exception as e:
return {
'success': False,
'exit_code': -1,
'stdout': '',
'stderr': f'执行失败: {str(e)}',
'method': 'native_ssh_windows' if os.name == 'nt' else 'native_ssh'
}
def check_ssh_agent() -> Tuple[bool, str]:
"""
检查 ssh-agent 是否运行且有密钥
Returns:
(is_available, message) 元组
"""
# Windows 特殊处理:直接检查 Windows SSH Agent 服务
if os.name == 'nt':
try:
# 检查服务状态
result = subprocess.run(
['powershell', '-NoProfile', '-Command',
'[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; ' +
'Get-Service ssh-agent | Select-Object Status | ConvertTo-Json'],
capture_output=True,
text=True,
encoding='utf-8',
errors='replace',
timeout=5
)
if result.returncode == 0:
import json
service_info = json.loads(result.stdout)
status = service_info.get('Status', 0)
if status == 4: # Running
# 使用 Windows 原生 ssh-add(Git 的 ssh-add 无法连接 Windows Agent)
native_ssh_add = _get_windows_native_ssh_path('ssh-add.exe')
ssh_add_cmd = f'& "{native_ssh_add}" -l' if native_ssh_add else 'ssh-add -l'
key_result = subprocess.run(
['powershell', '-NoProfile', '-Command', ssh_add_cmd],
capture_output=True,
text=True,
encoding='utf-8',
errors='replace',
timeout=5
)
if key_result.returncode == 0:
key_count = len([line for line in key_result.stdout.strip().split('\n') if line])
return True, f"Windows SSH Agent 运行中,已加载 {key_count} 个密钥"
elif key_result.returncode == 1:
return False, "Windows SSH Agent 运行中,但未加载任何密钥(运行 ssh-add 添加密钥)"
else:
return False, "Windows SSH Agent 服务未运行"
except Exception as e:
pass # 降级到 Unix 检测逻辑
# Unix/Linux 检测逻辑
auth_sock = os.environ.get('SSH_AUTH_SOCK')
if not auth_sock:
return False, "ssh-agent 未运行(SSH_AUTH_SOCK 未设置)"
# 尝试列出密钥
try:
result = subprocess.run(
['ssh-add', '-l'],
capture_output=True,
text=True,
timeout=5
)
if result.returncode == 0:
# 有密钥
key_count = len([line for line in result.stdout.strip().split('\n') if line])
return True, f"ssh-agent 运行中,已加载 {key_count} 个密钥"
elif result.returncode == 1:
# agent 运行但没有密钥
return False, "ssh-agent 运行中,但未加载任何密钥(运行 ssh-add 添加密钥)"
else:
return False, f"ssh-agent 状态异常: {result.stderr}"
except subprocess.TimeoutExpired:
return False, "ssh-add 命令超时"
except FileNotFoundError:
return False, "ssh-add 命令不存在"
except Exception as e:
return False, f"检查 ssh-agent 失败: {str(e)}"
"""
工具函数模块
提供SSH相关的辅助工具函数。
"""
import os
import platform
import subprocess
from typing import Optional
def check_ssh_available() -> bool:
"""
检查系统是否安装了SSH客户端
Returns:
安装了返回True,否则返回False
"""
try:
result = subprocess.run(
["ssh", "-V"],
capture_output=True,
text=True,
timeout=5
)
return True
except (subprocess.TimeoutExpired, FileNotFoundError):
return False
def get_ssh_version() -> Optional[str]:
"""
获取SSH客户端版本
Returns:
版本字符串,如果获取失败返回None
"""
try:
result = subprocess.run(
["ssh", "-V"],
capture_output=True,
text=True,
timeout=5
)
# SSH版本信息通常输出到stderr
version_str = result.stderr.strip() if result.stderr else result.stdout.strip()
return version_str
except Exception:
return None
def validate_key_file(key_file: str) -> tuple[bool, str]:
"""
验证SSH密钥文件
Args:
key_file: 密钥文件路径
Returns:
(是否有效, 错误信息)元组
"""
if not os.path.exists(key_file):
return False, f"密钥文件不存在: {key_file}"
if not os.path.isfile(key_file):
return False, f"密钥路径不是文件: {key_file}"
# 在Unix系统上检查文件权限
if platform.system() != "Windows":
stat_info = os.stat(key_file)
mode = stat_info.st_mode & 0o777
if mode & 0o077: # 检查group和other权限
return False, f"密钥文件权限过于宽松,应该设置为600: {key_file}"
return True, ""
def format_ssh_command(host: str, user: str, command: str,
key_file: Optional[str] = None,
port: int = 22) -> str:
"""
格式化SSH命令字符串(用于显示)
Args:
host: 服务器地址
user: 用户名
command: 要执行的命令
key_file: 密钥文件路径
port: 端口
Returns:
格式化的SSH命令字符串
"""
parts = ["ssh"]
if key_file:
parts.extend(["-i", key_file])
if port != 22:
parts.extend(["-p", str(port)])
parts.append(f"{user}@{host}")
parts.append(f'"{command}"')
return " ".join(parts)
def parse_ssh_output(output: str) -> dict:
"""
解析SSH命令输出
Args:
output: SSH命令的输出
Returns:
解析后的字典
"""
lines = output.strip().split('\n')
return {
'lines': lines,
'line_count': len(lines),
'first_line': lines[0] if lines else '',
'last_line': lines[-1] if lines else ''
}
Related skills
FAQ
How much faster is daemon mode?
Single-command latency drops from ~0.45s to ~0.12s, and 10 sequential commands from ~4.5s to ~1.2s.
Does it support jump hosts?
Yes. It handles multi-level jump hosts automatically via ProxyJump configuration.