每次 git pull 都要输密码:HTTP 凭据被成功后立刻 erase
macOS 上对内网 Gitea 走 HTTP(非 SSH、非标准 80 端口)时,git pull 明明已经 Already up to date,下次还是要输密码。Cursor 终端里还会先打一行 Electron 报错。凭据助手、钥匙串、明文 store 换了几轮都没用——密码其实存进去过,随即被 Git 自己删掉。
现象
- 远程是 HTTP,例如
http://git.internal:3000/org/repo.git,Gitea 返回标准401+WWW-Authenticate: Basic。 - 每次
git pull/git fetch都要输入一次密码,成功后提示Already up to date。 - 在 Cursor 集成终端里,密码提示前可能出现:
ERROR:electron/shell/app/node_main.cc:153
Node.js environment variables are disabled because this process is invoked by other apps.
~/.git-credentials在一次成功的 pull 之后仍是 0 字节。git credential approve能写入,但下一次访问又变空。
先排除的几条歧路
这些配置问题确实存在,但都不是「文件一直是空的」的根因。
关掉 AskPass 之后,密码会改到终端里输入,报错消失,但 每次还是要输——说明真正丢掉密码的环节在 Git 凭据协议,不在弹窗。
| 歧路 | 为什么看起来像原因 | 实际结果 |
|---|---|---|
Cursor GIT_ASKPASS | Electron 发现父进程是 git 不是 Cursor,清掉 NODE_OPTIONS 并打 ERROR | 只解释那行红字;关掉后仍要密码 |
osxkeychain | 系统默认助手,macOS 上「应该」能记住 | 对 HTTP + 主机为 IP + 自定义端口经常 store 失败;钥匙串弹窗还可能被 IDE 挡住 |
credential.helper=store | 明文文件最简单,approve 能写入 | 成功的 git pull / ls-remote 之后文件又被清成 0 字节 |
| 仓库 hook / Git LFS | core.hooksPath 或 filter.lfs.required | git pull 已 up to date 时不一定跑 hook;无 LFS 对象时也复现 |
系统级 credential.helper=osxkeychain 仍在。若只给该 HTTP 主机加 helper = store 而不先清空助手列表,osxkeychain 会继续参与 get / erase。需要时用空助手重置:
[credential "http://git.internal:3000"]
username = alice
helper =
helper = store
空的 helper = 会清掉继承来的系统助手,再挂上 store。这能让 git credential approve 写入 ~/.git-credentials,但 挡不住后面的 erase。
根因
给凭据助手包一层日志(只记 get / store / erase 和 host,不要记密码)后再 ls-remote,会看到固定节奏:
action=get host=git.internal:3000
action=store host=git.internal:3000 # 偶发,认证成功时
action=erase host=git.internal:3000 # 紧接着,几乎总会发生
后台若开着 IDE 的 git.autofetch,还会在几秒内打出几十对 get → erase。于是出现「刚才存上了、转头文件又是 0 字节」。
一次 git ls-remote 的交互可以概括成:
认证已经成功,Git 仍调用 erase(对应 git credential reject)。具体触发可能叠加了几件事,不必在生产里逐条钉死:
- Git 2.50(Apple Git)对 helper 发送
capability[]=authtype、capability[]=state,内置store并不按新能力协议应答。 git-remote-http对同一主机有多次 HTTP 往返;后续请求 401 会把刚store的条目reject掉。- Cursor 的
git.autofetch在无 TTY 的后台进程里 fetch,失败同样走erase,把用户刚存的密码冲掉。
对排障来说,只需记住:不是 store 没调用,而是 store 之后必有 erase。 只换 osxkeychain / store 解决不了。
处理方案
继续走 HTTP 即可,不必改成 SSH。思路是:IDE 不再劫持 AskPass;该主机只用自己的 helper;忽略误删。
1. 关掉 Cursor 集成 AskPass
{
"git.useIntegratedAskPass": false,
"git.terminalAuthentication": true
}
改完后 新开终端,旧会话里的 GIT_ASKPASS 还在。这能消掉 Electron 那行 ERROR,避免 AskPass 把空密码写进 store。
git.autofetch 可以继续开。忽略 erase 之后,后台 fetch 不再把密码清掉。
2. 该 HTTP 主机专用 helper,忽略 erase
~/.local/bin/git-credential-http-keep:
#!/bin/sh
# 内网 Git HTTP:Git / IDE 后台 fetch 会在成功后误调用 erase。
# 忽略 erase,只处理 get / store。
ACTION="${1-}"
FILE="${HOME}/.git-credentials"
DATA=$(cat)
case "$ACTION" in
erase)
exit 0
;;
get|store)
printf '%s\n' "$DATA" | git credential-store --file "$FILE" "$ACTION"
;;
*)
exit 0
;;
esac
chmod 755 ~/.local/bin/git-credential-http-keep
git config --global --unset-all credential.http://git.internal:3000.helper
git config --global credential.http://git.internal:3000.helper ''
git config --global --add credential.http://git.internal:3000.helper ~/.local/bin/git-credential-http-keep
git config --global credential.http://git.internal:3000.username alice
把 git.internal:3000 和 alice 换成自己的主机与用户名。绝对路径也可以,Git 把以 / 开头的 helper 当可执行文件,不会再套 git credential- 前缀。
3. 写入一次密码
git pull 不会稳定地留下 store(即使成功也会 erase)。用 git credential-store 直接写文件:
read -s "Git 密码或 Token: " GIT_PW; echo
printf "protocol=http\nhost=git.internal:3000\nusername=alice\npassword=%s\n\n" "$GIT_PW" \
| git credential-store --file ~/.git-credentials store
unset GIT_PW
wc -c ~/.git-credentials
wc -c 应大于 0。密码里有 %、\ 时,用 Python 往 helper 的 stdin 写 credential 协议更稳,避免 printf 转义。
Gitea 开了 2FA 时,这里填 Personal Access Token,不是登录密码。~/.git-credentials 是明文,权限保持 600,只适合内网 HTTP。
验证
无 TTY 也能通过,说明 get 生效,且文件没被后续 erase 清掉:
GIT_TERMINAL_PROMPT=0 git ls-remote origin HEAD
GIT_TERMINAL_PROMPT=0 git pull
wc -c ~/.git-credentials
两次访问都不该再出现 Password for 'http://alice@git.internal:3000',字节数应保持不变。
代价与何时不该这样做
忽略 erase 之后,错误密码不会被 Git 自动清掉。下次 get 仍返回旧值,可能反复 401。密码或 Token 轮换后,用上面的 credential-store store 覆盖写入即可。
更稳的长期做法仍是 SSH 公钥。本文场景是:远程只提供 HTTP、必须继续用账号或 Token,又不能接受每次 pull 都交互输入。这时「明文 store + 忽略误删」是可接受的本机折中,不是通用安全基线。