遇到这类问题,通常不是密钥算法有问题,也不是权限设置有问题,而是你在处理一个被忽略的“环境编码”问题。我也是在一台 Windows 机器上折腾 SSH Keygen 时才发现,问题根源是 Git、Windows 和 SSH 三方对中文路径的认知完全不在一个频道上。账户目录叫 C:\Users\张伟,命令行里显示得很正常,但执行 ssh-keygen -t ed25519 就是会在创建 .ssh 目录时报错。如果你也恰好在中文用户名下生成密钥,并且卡在路径创建上,这篇文章能帮你把来龙去脉和解决办法一次理清。
1. 从一条报错开始:症状、误判与真正的定位线索
1.1 报错现场和直观猜测
先说现象。我在 Git Bash 里执行的是这条命令:
bash复制ssh-keygen -t ed25519 -C "dev@example.com"
正常情况下,它会提示你输入保存路径,回车即可使用默认位置。但在我这台系统用户名是中文的机器上,敲完回车后出现了下面这种输出:
code复制Generating public/private ed25519 key pair.
Enter file in which to save the key (/c/Users/张伟/.ssh/id_ed25519):
Could not create directory '/c/Users/张伟/.ssh'.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /c/Users/张伟/.ssh/id_ed25519
注意,日志里有两处很矛盾:前面说 Could not create directory,但后面又说密钥文件“已经保存”。这是因为 ssh-keygen 在尝试创建 .ssh 目录失败后,并没有直接退出,而是继续尝试写入文件,最后可能写到了错误的位置,或者在某些更严格的版本里直接返回 Saving key failed: No such file or directory。
如果你和我一样看到 Could not create directory,第一反应大概率是怀疑目录权限、杀毒软件拦截或者 Git 安装的问题。于是我开始检查 C:\Users\张伟 的权限,看是不是当前用户没有写权限,又去关闭了实时防护软件,甚至重装了一次 Git for Windows,问题依旧。
1.2 第一轮排查:我最初的错误方向
我一开始做了一套“标准操作”:
- 检查
C:\Users\张伟是否可写,确认当前用户拥有完全控制权限。 - 手动创建
.ssh文件夹,排除目录不存在的问题。 - 在 Windows 凭据管理器里翻了一遍,确认没有缓存导致的问题。
- 重新以管理员身份运行 Git Bash,结果仍然一样。
这套操作基本是在瞎忙。因为报错的关键并不是 Windows 权限,而是 Git Bash 环境里 ssh-keygen 拿到的默认路径本身就有问题。
真正让我把头绪理清楚的是下面这一步。
1.3 把问题剥离到最小命令
我先在 Git Bash 里确认当前用户主目录:
bash复制echo $HOME
输出是:
code复制/c/Users/张伟
看起来正常,终端能正常显示中文。于是我又试着手动创建目录:
bash复制mkdir -p "$HOME/.ssh"
echo $?
mkdir 返回 0,也就是说这个命令成功了。那为什么 ssh-keygen 创建目录会失败?到这里,直觉告诉我,问题很可能不在目录权限上,而在 ssh-keygen 这个程序自己处理路径的方式上。
为了验证,我换了一个纯英文路径去生成密钥:
bash复制mkdir -p /c/Users/testssh
ssh-keygen -t ed25519 -C "dev@example.com" -f /c/Users/testssh/testkey
结果一次成功,没有任何报错。
到这里基本可以锁定:只要路径里没有中文,ssh-keygen 就会正常工作;一旦路径中含有中文,它就会在创建目录这个环节失败。接下来要搞清楚的问题只有一个——为什么中文路径对 ssh-keygen 这么不友好。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 为什么偏偏是中文目录在捣乱:Git、Windows 与 SSH 的路径认知差异
2.1 中文用户名是怎么进入路径的
如果你在安装 Windows 或创建本地账户时直接使用了中文用户名,系统会把这个名字直接用作用户主目录名的一部分,常见结果就是:
code复制C:\Users\张伟
C:\Users\李雷
C:\Users\王芳
有些微软账户即使显示名称是中文,实际生成的用户目录可能是拼音或邮箱前缀,比如 C:\Users\zhangsan,那就没有问题。但只要你的用户目录中出现了非 ASCII 字符,Git 相关工具就有概率踩坑。
从设计上看,NTFS 文件系统本身对文件名使用 UTF-16 编码,中文路径对它而言没有任何障碍。你会在资源管理器里自由查看和访问这些目录,Windows 自带的应用也基本没问题。问题往往出现在那些从 Unix/Linux 世界“移植”过来的工具链上,Git Bash 恰好就是这样一套工具链。
2.2 Git Bash、MSYS2 和 Windows OpenSSH 各自是什么角色
Git for Windows 在安装时,会携带一个基于 MSYS2 的模拟环境。这个环境让你在 Windows 上也能用 /c/Users/xxx 这样的路径风格,还能运行 ls、grep、ssh 等命令。你执行的 ssh-keygen,有可能是 Git 自带的 MSYS 版本,也有可能是 Windows 系统自带的 OpenSSH 版本,取决于环境变量 PATH 的先后顺序。
如果你在 Git Bash 里执行:
bash复制which ssh-keygen
会看到类似 /usr/bin/ssh-keygen 或 /c/Windows/System32/OpenSSH/ssh-keygen.exe 的输出。这两个版本的责任范围不一样:
- Git 自带的版本依赖 MSYS2 运行时。
- Windows 系统自带的 OpenSSH 是原生 Windows 程序,走的是 Windows API。
但两个版本都有可能遇到中文路径问题,只是发生机制略有差异。核心原因可以简化成一句话:这些程序在调用底层 API 创建设备目录时,会把路径从 UTF-8 内部表示转换成 Windows 当前 ANSI 代码页能表示的字符串。如果当前系统代码页不是 UTF-8,而路径里正好有中文字符,转换过程就可能出现字符丢失或无法映射的情况,最终底层的 CreateDirectory 收到的是一个无法解析的路径,于是返回“目录不存在”或“路径错误”。
你可能觉得奇怪,既然 Git Bash 本身能显示中文路径,为什么底层转换还会出问题?因为终端显示和文件系统 API 调用是两码事。终端只需要把字节渲染出来,而 API 调用要做严格的编码转换。Git Bash 内部使用 UTF-8,Windows 原生程序使用 UTF-16,这两个编码之间本来可以直接互转,但如果中间夹了一层“ANSI 代码页”的窄字符转换逻辑,就容易在中文路径上翻车。
2.3 正斜杠与反斜杠只是表象
还有一个容易混淆的点:很多人会把这个问题归咎于路径分隔符。比如报错信息里的 /c/Users/张伟/.ssh 看起来不像 Windows 原生路径,有人觉得自己用反斜杠写 C:\Users\张伟\.ssh 就能解决。
我之前也试过在 ssh-keygen 提示输入保存路径时,手动粘贴反斜杠路径,比如输入 C:\Users\张伟\.ssh\id_ed25519。结果同样失败。原因在于,问题的核心是“非 ASCII 路径经过窄字符转换后失效”,而不是分隔符写错。只要路径中包含中文,正斜杠、反斜杠都是绕不过去的。
2.4 一个容易混淆的 Git 配置项:core.quotepath
这里顺便提一个很容易被搞混的配置:core.quotepath。
bash复制git config --global core.quotepath false
这个配置的作用是让 git status、git log 等命令在输出中文文件名时,显示原始中文而不是 \346\265\213 这类转义八进制序列。它和 ssh-keygen 创建目录没有任何关系。网上搜中文路径问题时,经常会看到有人建议加这一行,但它解决的是“显示”层的问题,不是“创建”层的问题。
我见过有人把 core.quotepath 改来改去,折腾半天仍然报错,就是这个原因。理解这个误区之后,你就不会再被带偏了。
3. 最推荐的办法:把 HOME 指向一个纯英文目录
3.1 思路:让 ssh-keygen 忘记中文用户目录
当默认路径是 /c/Users/张伟/.ssh 时,无论你怎么调整目录权限,ssh-keygen 都可能在创建目录时失败。与其想办法让这个路径“可用”,不如直接换一个纯英文路径作为它的主目录。
ssh-keygen 默认保存密钥的位置,由 HOME 环境变量决定。在 Git Bash 里运行:
bash复制echo $HOME
如果结果里含有中文,那就意味着 Git、SSH 和许多其他 Unix 风格工具都会默认在这个路径下读写配置。我们可以把 HOME 指向一个新创建的纯英文目录,比如 C:\Users\ssh-keys。
注意:这个目录不一定要放在
C:\Users下面,你可以放在任意你喜欢的位置,比如D:\ssh-home。唯一原则是路径中不要包含中文或空格,避免引入新的不可控因素。
3.2 创建目录并设置为 HOME
我建议用两步走的方式,避免在当前会话里留下不干净的环境变量。
先在 PowerShell 或 CMD 中执行:
powershell复制mkdir C:\Users\ssh-keys -Force
setx HOME "C:\Users\ssh-keys"
setx 会把 HOME 写入当前用户的环境变量,但它不会修改当前已打开的终端进程。也就是说,执行完 setx 后,你必须在当前窗口里手动 exit,然后重新打开 Git Bash、PowerShell 或 Windows Terminal,新的终端进程才会读到 HOME=C:\Users\ssh-keys。
重新打开 Git Bash 后,验证一下:
bash复制echo $HOME
正常输出应该是:
code复制/c/Users/ssh-keys
如果还是显示 /c/Users/张伟,说明环境变量没有被读取到。这时你需要检查是否真的重新启动了终端,还要确认变量设置的是“用户变量”而不是“系统变量”。
如果你更习惯图形界面,可以这样操作:
- 右键“此电脑” → “属性”。
- 点击“高级系统设置”。
- 点击“环境变量”。
- 在“用户变量”区域点击“新建”。
- 变量名填
HOME,变量值填C:\Users\ssh-keys。 - 一路点“确定”。
这种方法本质上和 setx 一样,适合不想敲命令的人。
3.3 生成密钥并验证
完成环境变量修改后,在新的 Git Bash 窗口中生成密钥:
bash复制mkdir -p "$HOME/.ssh"
chmod 700 "$HOME/.ssh"
ssh-keygen -t ed25519 -C "dev@example.com" -f "$HOME/.ssh/id_ed25519"
这次不会再出现 Could not create directory 的报错。生成完成后,查看公钥:
bash复制cat "$HOME/.ssh/id_ed25519.pub"
把它复制到代码托管平台(GitHub、Gitee、GitLab 等)即可。
3.4 这个方案有哪些副作用
设置 HOME 为全新目录后,最直接的影响是:原来存放在 C:\Users\张伟\.gitconfig 下的 Git 全局配置不会再被读取。因为 Git 在 Windows 上找全局配置时,也是从 $HOME/.gitconfig 出发的。你需要在新的 HOME 下重新配置用户名和邮箱:
bash复制git config --global user.name "你的名字"
git config --global user.email "你的邮箱"
如果之前已经配置过,你也可以直接把旧文件复制过来。我习惯用 PowerShell 复制:
powershell复制Copy-Item -Path "C:\Users\张伟\.gitconfig" -Destination "C:\Users\ssh-keys\.gitconfig"
同理,如果旧路径下已经存在可以正常使用的 .ssh 目录,也要一并复制过来:
powershell复制Copy-Item -Path "C:\Users\张伟\.ssh" -Destination "C:\Users\ssh-keys\.ssh" -Recurse
复制完成后,建议测试一下密钥是否还能和远程仓库正常认证。用 ssh -T 验证:
bash复制ssh -T git@github.com
如果输出 Hi xxx! You've successfully authenticated, but GitHub does not provide shell access.,说明迁移成功。
3.5 为什么我执着于推荐改 HOME
改 HOME 表面上有点“绕”,但它能一次性解决很多相关工具的路径问题,而不是只照顾 ssh-keygen 这一个命令。
比如 PowerShell 里的某些模块、VS Code 的 Remote-SSH 插件、SourceTree 等 GUI 客户端,它们在寻找 SSH 密钥时,都会默认去当前用户的主目录下找 .ssh。如果你只是用 -f 参数把密钥生成到某个临时目录,下次换一个工具可能又找不到了。而设置 HOME 之后,大家都会统一到 C:\Users\ssh-keys 去找,属于“一次配置、到处受益”。
4. 如果你不能改环境变量:指定密钥路径与逐仓库覆盖
4.1 受限制环境下能用吗
不是所有人都能随意修改环境变量。在公司电脑上,域策略可能禁用了 setx,或者你在使用一台不允许修改用户配置的机器。这种情况下,可以绕开 HOME 路径问题,直接给 ssh-keygen 指定一个纯英文保存路径。
先生成密钥:
bash复制mkdir -p /c/Users/ssh-keys/.ssh
ssh-keygen -t ed25519 -C "dev@example.com" -f /c/Users/ssh-keys/.ssh/id_ed25519 -N ""
这里我在 -f 后面指定了完整的英文路径;-N "" 表示不设置 passphrase,适合纯自动化场景。如果希望有口令保护,可以去掉这个参数,交互式输入。
4.2 使用密钥连接远程主机
这样生成的密钥和默认路径没有关系。连接远程主机时,需要告诉 ssh 去哪个文件找私钥,标准做法是加 -i 参数:
bash复制ssh -i /c/Users/ssh-keys/.ssh/id_ed25519 -T git@github.com
如果你是在某个 Git 仓库里工作,不想每次都敲 -i,可以在仓库目录下设置 Git 的 SSH 命令:
bash复制git config core.sshCommand "ssh -i C:/Users/ssh-keys/.ssh/id_ed25519"
设置后,这个仓库的 git fetch、git pull、git push 都会自动带上这个私钥路径。
也可以只针对某个远程仓库:
bash复制git config core.sshCommand "ssh -i C:/Users/ssh-keys/.ssh/id_ed25519"
这里无论仓库是 origin 还是其他 remote,都会被这个配置统一接管。
4.3 使用 SSH config 管理多个密钥
如果机器上需要管理多个免密密钥,比如一个用于 GitHub,一个用于公司内网 GitLab,写 SSH config 更好管理。
在纯英文目录下创建一个 config 文件,路径假设为 C:\Users\ssh-keys\.ssh\config,内容大致如下:
code复制Host github.com
HostName github.com
User git
IdentityFile C:/Users/ssh-keys/.ssh/id_ed25519
Host gitlab.company.com
HostName gitlab.company.com
User git
IdentityFile C:/Users/ssh-keys/.ssh/id_company
然后在使用 ssh 时将 -F 指定到该 config,或通过环境变量:
bash复制ssh -F /c/Users/ssh-keys/.ssh/config -T git@github.com
这种方案的局限是:很多 GUI 工具并不会读取你手动指定的
-i参数。如果你只是使用命令行,问题不大;如果必须在 VS Code 的 Remote-SSH、SourceTree、Fork 这类工具里使用,临时方案可能不够稳定。长远看,能设置 HOME 还是优先设置 HOME。
4.4 临时方案适合哪类用户
当你有以下情况时,临时方案反而是首选:
- 公司电脑不能改环境变量,只能使用命令行。
- 只需要在一台机器上的某个仓库里提交代码。
- 密钥是特定项目专用,不希望影响全局配置。
我自己有一台机器就是这种限制环境。我直接把密钥放到了 D:\work-keys\prod\id_rsa,然后为几个核心仓库执行了 git config core.sshCommand。这样既绕开了中文用户目录,也没有影响到系统里其他开发环境。
5. 密钥文件生成了,还有一个绕不开的权限与工具混用问题
5.1 当 Windows OpenSSH 报出权限错误
把 HOME 切成英文目录后,中文路径问题基本解决,但有一个新的高频问题会浮出水面:如果你平时既用 Git Bash,又用 Windows 自带的 OpenSSH,可能出现下面的报错:
code复制Bad owner or permissions on C:\Users\ssh-keys\.ssh\config
这个报错并不是说当前用户没有权限读这个文件,而是说文件权限“过于宽松”。Windows OpenSSH 沿用了 Unix 世界里对私钥和 config 文件的保护逻辑:如果私钥文件或配置文件可以被其他用户或管理员组读取,ssh 就会拒绝使用,防止私钥泄露。
在 Git Bash 里生成的密钥文件,默认权限继承自父目录,可能带了 Authenticated Users、Users 等组权限。这些权限在 Windows 上看起来没问题,但 Windows OpenSSH 不认账。
5.2 用 icacls 收窄权限
我建议用 icacls 对 .ssh 目录和里面的关键文件做一次权限重置。在管理员 PowerShell 中执行:
powershell复制# 先移除目录的所有继承权限
icacls "C:\Users\ssh-keys\.ssh" /inheritance:r
# 只给当前用户完全控制权
icacls "C:\Users\ssh-keys\.ssh" /grant:r "$($env:USERNAME):(OI)(CI)F"
# 对私钥文件单独收窄
icacls "C:\Users\ssh-keys\.ssh\id_ed25519" /inheritance:r
icacls "C:\Users\ssh-keys\.ssh\id_ed25519" /grant:r "$($env:USERNAME):R"
其中 (OI)(CI)F 表示目录和子文件继承完全控制,R 表示只读。私钥文件只需要读权限,不需要写入权限,所以只给 R 是合理的。如果私钥文件也需要被 ssh-agent 修改时间戳或写入 known_hosts,那是另一个场景,这里不展开。
处理完之后,再用 ssh -T 测试一次。如果权限报错消失,说明就绪。
5.3 让 Git 统一使用同一个 SSH 实现
Windows 上可能同时存在多个 ssh:
- Git for Windows 自带的版本,路径通常在
C:\Program Files\Git\usr\bin\ssh.exe。 - Windows 系统自带的 OpenSSH,路径通常在
C:\Windows\System32\OpenSSH\ssh.exe。
默认情况下,Git 在调用 ssh 时会通过 PATH 搜索。如果 PATH 里系统目录排在 Git 目录前面,Git 会优先使用 Windows 系统自带的 OpenSSH。两个版本在路径解析、配置文件读取、权限检查方面存在细微差异,容易出现“在 Git Bash 里测试没问题,但用 Git 推送时却报错”的情况。
为了统一行为,可以在 Git 仓库或全局层面指定 Git 自带的 SSH:
bash复制git config --global core.sshCommand "C:/Program Files/Git/usr/bin/ssh.exe"
或者通过环境变量:
bash复制setx GIT_SSH_COMMAND "C:\Program Files\Git\usr\bin\ssh.exe"
设置后重启终端再测试。这招尤其适合“命令行里 ssh -T 能通过,但 git push 失败”的诡异场景。
5.4 一个容易被忽略的细节:系统区域设置的 Beta UTF-8 选项
如果你不想改 HOME,也可以尝试在 Windows 系统区域设置里开启 Beta UTF-8 选项:
- 打开“控制面板” → “时钟和区域” → “区域”。
- 切到“管理”选项卡,点击“更改系统区域设置”。
- 勾选“Beta: 使用 Unicode UTF-8 提供全球语言支持”。
- 重启系统。
这个选项会把系统 ANSI 代码页切换为 UTF-8,很多因为中文路径导致的命令行问题都会缓解。但我并不建议把这个作为首选方案,原因很现实:
- 它需要重启系统,影响面比较大。
- 某些老版本软件对 UTF-8 代码页兼容性不好,可能出现显示乱码或其他问题。
- 如果是公司统一管理的电脑,你未必有权限修改或重启。
我把这个选项放在“可以尝试”的层面,而不是“解决问题”的主力方案。相比起来,设置 HOME 的精准度和副作用都可控得多。
6. 我现在的配置习惯和后续防坑建议
6.1 每台新 Windows 机器的固定动作
踩过这次坑之后,我养成了一个习惯:装完 Git for Windows 后,第一件事不是急着配置用户名邮箱,而是先运行一下:
bash复制echo $HOME
只要 $HOME 里出现非英文字符,我会直接创建一个纯英文工作主目录,并执行:
powershell复制setx HOME "C:\Users\ssh-keys"
然后重新打开 Git Bash,把 .gitconfig 和 .ssh 准备好。这样可以确保后续无论用命令行还是 GUI 工具,都不会再撞上“中文路径创建失败”的问题。
新的检查清单大致是:
echo $HOME确认主目录为英文路径。mkdir -p "$HOME/.ssh"创建.ssh目录。- 执行
ssh-keygen -t ed25519 -C "your_email@example.com"生成密钥。 cat "$HOME/.ssh/id_ed25519.pub"复制公钥到代码托管平台。ssh -T git@github.com或对应平台测试连接。- 检查
git config --global user.name和user.email是否设置。
这样做大约只需要两分钟,却能避免深夜被一个莫名其妙的目录问题卡住。
6.2 创建目录联接的小技巧,但我为什么不推荐当首选
还有一种偏门做法是在原有中文用户目录旁边创建一个带有 ASCII 名字的目录联接:
cmd复制mklink /J C:\Users\ssh-keys C:\Users\张伟
然后把 HOME 指向 C:\Users\ssh-keys。
从理论上说,这种方式可以让旧的 .gitconfig、.ssh 文件夹继续留在原目录中,同时通过一个新的 ASCII 入口去访问它们。我试过一次,某些场景下确实能正常工作,尤其是当原来的 C:\Users\张伟\.ssh 目录里已经存在可用的密钥和配置文件时,这样做可以免去复制文件这步操作。
但我不建议把目录联接作为首选方案,原因有两个。第一,目录联接的创建需要管理员权限,在公司电脑上可能被策略限制;第二,它的解析链条比普通目录更长,碰到行为不规范的软件时,有时仍然会暴露目标路径的中文部分,问题没有真正从根源消失。既然改 HOME 的成本并不高,直接使用一个干净目录反而是最省心的选择。
6.3 以后再遇到类似路径问题的排查思路
这次问题给我的教训很直接:当某个命令行工具在“创建目录”这种基础操作上失败时,先检查它使用的默认路径里有没有非 ASCII 字符,比一开始就去怀疑权限或安全软件高效得多。
现在我再遇到命令行工具报出“路径创建失败”类错误,会先按这个顺序排查:
- 查看报错信息中出现的完整路径,是否包含中文或特殊字符。
- 用
echo $HOME或echo $env:USERPROFILE确认当前用户主目录状态。 - 尝试切换到纯英文路径下重新执行同一条命令,快速判断是不是路径编码问题。
- 如果确实和中文路径有关,优先通过修改 HOME 或指定绝对英文路径绕开。
- 测试验证后,再处理密钥权限、多 SSH 工具冲突等后续问题。
路径问题看起来是一个小坑,但在 Windows 上,它往往牵涉系统环境变量、终端模拟层和底层 API 的编码行为。把排查顺序理顺,就不会再被类似问题耗掉几个小时了。
