今天碰到一个很离谱的问题:Mac mini 上的 code-server 原本用得好好的,WebSSH 里突然开始各种报错。

最典型的是这两个:

Error: The current working directory must be readable to yy to run brew.
fatal: Unable to read current working directory: Operation not permitted

看起来像是 brew 有问题,又像是 Git 仓库坏了,还像是当前目录权限不对。实际最后查下来,根本不是这些。

真正的原因是:code-server 从 brew 安装换成官方脚本安装以后,背后的 Node 路径变了,macOS TCC 对新路径没有放行,甚至明确拒绝了它访问 ~/Documents

就这点破事,能把 WebSSH、Git、brew 一起干废。

先说现场

项目路径在:

/Users/yy/Documents/Bonjourr

传统权限看起来一点问题没有:

/Users/yy/Documents
drwx------@ yy staff

/Users/yy/Documents/Bonjourr
drwxr-xr-x@ yy staff

用户也是 yy,目录 owner 也是 yy,按普通 Linux/Unix 的直觉,这事应该结束了。

但 WebSSH 里执行:

git -C /Users/yy/Documents/Bonjourr status --short

还是报:

fatal: Unable to read current working directory: Operation not permitted

更恶心的是,报错点甚至不在 Bonjourr 这个仓库,而是在当前 shell 正站着的 ~/Documents。也就是说,git -C 还没来得及真正去目标目录,就先死在 getcwd 这一步了。

一开始能修的都修了

我把问题丢给 Codex,它先按常规文件系统方向排:

ls -ldeO@ /Users/yy/Documents
ls -ldeO@ /Users/yy/Documents/Bonjourr
xattr -l /Users/yy/Documents/Bonjourr

能看到一些 macOS 特有的东西:

com.apple.macl
com.apple.provenance

项目目录里没有明显的 uchgschgrestricted,ACL 也不是那种一眼能看出禁止读取的状态。

然后做了一轮保守修复:

chmod -RN /Users/yy/Documents/Bonjourr
chflags -R nouchg /Users/yy/Documents/Bonjourr
chown -R yy:staff /Users/yy/Documents/Bonjourr
chmod -R u+rwX /Users/yy/Documents/Bonjourr
xattr -dr com.apple.quarantine /Users/yy/Documents/Bonjourr

中间还有个小坑:第一次清 xattr 的时候,.git/objects 里大量文件报 Permission denied。后来发现这些 Git object 是只读文件,恢复用户写权限以后,xattr 清理才成功。

这一步修完以后,在普通 shell 里测:

git -C /Users/yy/Documents/Bonjourr status --short
brew --version

都正常。

于是我以为事情结束了。

然后 WebSSH 里一测,还是不行。

真正的线索在 code-server 日志里

这时候问题就很明确了:不是文件坏了,是 code-server/WebSSH 这条进程链自己被 macOS 卡住了。

Codex 接着去看 code-server 的启动方式:

launchctl print gui/501/com.yy.code-server

发现 code-server 是一个用户级 LaunchAgent,工作目录是:

/Users/yy

这本身没问题。

但再去翻 code-server 的状态和日志,看到它默认打开的是整个:

/Users/yy/Documents

日志里已经把话说死了:

EPERM: operation not permitted, scandir '/Users/yy/Documents'

这就不是 chmod 的事了。这是 macOS TCC 在拦。

最后查到 TCC 里有明确拒绝

关键命令是查用户自己的 TCC 数据库:

sqlite3 ~/Library/Application\ Support/com.apple.TCC/TCC.db \
  "select service,client,client_type,auth_value,auth_reason,flags,last_modified
   from access
   where service like '%Documents%'
      or client like '%code-server%'
      or client like '%node%';"

结果里面有两条特别关键:

kTCCServiceSystemPolicyDocumentsFolder|/Users/yy/.local/lib/code-server-4.112.0/lib/node|1|2|2|0|...
kTCCServiceSystemPolicyDocumentsFolder|/Users/yy/.local/lib/code-server-4.116.0/lib/node|1|0|2|0|...

这里 auth_value=2 是允许,auth_value=0 是拒绝。

也就是说,旧版 code-server-4.112.0/lib/node 能访问 Documents,新版 code-server-4.116.0/lib/node 被拒绝访问 Documents

这下就全通了。

我只是把 code-server 从 brew 安装换成官方脚本安装,背后的可执行文件路径变了,macOS 就把它当成另一个东西。然后 TCC 里新路径是拒绝状态,于是 WebSSH 里所有依赖当前目录的命令都开始炸。

真他妈离谱。

修复方式

先备份 TCC 数据库:

cp ~/Library/Application\ Support/com.apple.TCC/TCC.db \
   ~/Library/Application\ Support/com.apple.TCC/TCC.db.bak-codeserver-documents-20260521

然后只改这一条 code-server 4.116 bundled Node 的权限:

sqlite3 ~/Library/Application\ Support/com.apple.TCC/TCC.db \
  "update access
   set auth_value=2,
       auth_reason=2,
       last_modified=strftime('%s','now')
   where service='kTCCServiceSystemPolicyDocumentsFolder'
     and client='/Users/yy/.local/lib/code-server-4.116.0/lib/node'
     and client_type=1;"

再刷新 TCC 缓存并重启 code-server:

killall tccd
launchctl kickstart -k gui/501/com.yy.code-server

另外,Codex 顺手把 code-server 默认打开目录从:

/Users/yy/Documents

改成了:

/Users/yy/Documents/Bonjourr

这样新开的终端不会再默认站在 Documents 根目录上,少踩一个坑。

验证

最后用 code-server 同一个 Node 二进制模拟执行:

/Users/yy/.local/lib/code-server-4.116.0/lib/node -e \
"const cp=require('child_process');
 cp.execFileSync('/usr/bin/git',
 ['-C','/Users/yy/Documents/Bonjourr','status','--short'],
 {cwd:'/Users/yy/Documents',stdio:'inherit'});
 console.log('git-ok')"

输出:

git-ok

再测 brew:

/Users/yy/.local/lib/code-server-4.116.0/lib/node -e \
"const cp=require('child_process');
 cp.execFileSync('/opt/homebrew/bin/brew',
 ['--version'],
 {cwd:'/Users/yy/Documents',stdio:'inherit'});
 console.log('brew-ok')"

输出:

Homebrew 5.1.7
brew-ok

这说明不是我当前这个 shell 碰巧能用,而是 code-server 实际使用的 Node 路径也已经能读 Documents 了。

最后收一下

这次问题如果只看表象,会很容易被带偏:

brew 报当前目录不可读
git 报 unable to read current working directory
目录 owner/mode 又完全正常

但真正的链路是:

code-server 安装方式变化
-> bundled Node 路径变化
-> macOS TCC 把新 Node 当成新客户端
-> Documents 权限是拒绝
-> WebSSH 里当前目录不可读
-> brew/git 全部跟着报错

这事最烦的地方就在于,它不是传统权限问题。chmodchownxattr 都只能排除干扰,最后真正要看的还是 TCC。

我在旁边能干什么呢?

我只能说:

卧槽,牛逼。
真牛逼。
这都能给你查出来。

然后再补一句:code-server 只是从 brew 安装换成官方脚本安装,就能整出这么个屁事,macOS 权限模型有时候是真的让人血压上来。