跳到正文

每次 git pull 都要输密码:HTTP 凭据被成功后立刻 erase

大杂烩4 min
目录(9)

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 能写入,但下一次访问又变空。

先排除的几条歧路

这些配置问题确实存在,但都不是「文件一直是空的」的根因。

每次 pull 都要密码

终端里有 Electron / AskPass 报错?

Cursor 注入了 GIT_ASKPASS

用 Electron 冒充 Node 弹凭据框

凭据助手是谁?

osxkeychain

store 明文文件

HTTP + IP + 非 80 端口

钥匙串经常静默写失败

成功后文件是否仍为空?

不是没存上,而是存完被 erase

助手生效,问题在别处

关掉 AskPass 之后,密码会改到终端里输入,报错消失,但 每次还是要输——说明真正丢掉密码的环节在 Git 凭据协议,不在弹窗。

歧路为什么看起来像原因实际结果
Cursor GIT_ASKPASSElectron 发现父进程是 git 不是 Cursor,清掉 NODE_OPTIONS 并打 ERROR只解释那行红字;关掉后仍要密码
osxkeychain系统默认助手,macOS 上「应该」能记住对 HTTP + 主机为 IP + 自定义端口经常 store 失败;钥匙串弹窗还可能被 IDE 挡住
credential.helper=store明文文件最简单,approve 能写入成功的 git pull / ls-remote 之后文件又被清成 0 字节
仓库 hook / Git LFScore.hooksPathfilter.lfs.requiredgit 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 / erasehost,不要记密码)后再 ls-remote,会看到固定节奏:

action=get    host=git.internal:3000
action=store  host=git.internal:3000   # 偶发,认证成功时
action=erase  host=git.internal:3000   # 紧接着,几乎总会发生

后台若开着 IDE 的 git.autofetch,还会在几秒内打出几十对 geterase。于是出现「刚才存上了、转头文件又是 0 字节」。

一次 git ls-remote 的交互可以概括成:

Gitea凭据文件凭据助手Git用户Gitea凭据文件凭据助手Git用户成功之后仍会走 rejectgit pull / ls-remote1GET info/refs2401 WWW-Authenticate Basic3get4读取5空或已存密码6username / password7带 Basic 重试82009store10写入11erase12清空文件13Already up to date14

认证已经成功,Git 仍调用 erase(对应 git credential reject)。具体触发可能叠加了几件事,不必在生产里逐条钉死:

  • Git 2.50(Apple Git)对 helper 发送 capability[]=authtypecapability[]=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:3000alice 换成自己的主机与用户名。绝对路径也可以,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 + 忽略误删」是可接受的本机折中,不是通用安全基线。

分享到