OpenClaw 生产升级血泪史:3097 个 root 文件与 exec-approvals.json 死锁的完整拆解
一段真实的修复实录:3097 个文件归属错乱、exec-approvals.json 死锁、systemd 权限盲区——每一个自建 OpenClaw 用户迟早都会遇到的事情。
🔴 写在前面:一个问题全景
如果你的 OpenClaw 是跑在云服务器上的(比如阿里云 OpenCloudOS),并且你用的还是 root 用户安装的 Nginx、Node.js、npm,那你几乎一定会遇到本文要讲的问题——区别只是早晚。
简单一句话概括这场事故:
openclaw update → 被迫用 sudo 安装 → root 在新版启动时污染了 openclaw 的状态目录 → 遗留 3097 个 root 归属文件 → exec-approvals.json 被 root 锁定 → 整个网关瘫痪,修这个文件需要执行命令,但执行命令需要这个文件可读,死锁形成
这不是一个 bug,而是一整套权限设计惯性在真实环境下的共振。每个环节单独看都合理,串在一起就成了灾难。
🧬 根因分析链条
第一环:npm global 的「所有权陷阱」
这是整件事的起点。绝大多数云服务器的 node 都是由 root 通过 nvm 或包管理器安装到 /www/server/nodejs/ 目录下的——这些目录的 owner 是 root。npm 的全局包安装到 /www/server/nodejs/v24.18.0/lib/node_modules/,同样是 root 所有。
# 查看所有权 $ ls -ld /www/server/nodejs/v24.18.0/lib/node_modules/ drwxr-xr-x 7 root root 4096 Jul 16 14:22 /www/server/nodejs/.../node_modules/ $ ls -ld /www/server/nodejs/v24.18.0/bin/node -rwxr-xr-x 1 root root 76M Jul 16 14:22 /www/server/nodejs/.../bin/node
但 OpenClaw 网关通过 systemd 以 openclaw 用户运行:
User=openclaw Group=openclaw
openclaw 用户对 npm 全局目录没有写权限。于是 openclaw update 执行时——这是内置的升级命令——在尝试 npm install -g openclaw@latest 时直接报错:
Error: EACCES: permission denied, mkdir '/www/server/nodejs/v24.18.0/lib/node_modules/.staging'
第二环:sudo npm i -g 的「用户切换盲区」
遇到 EACCES 之后,正常的 ops 思维是「那我用 sudo 装」。于是:
sudo systemctl stop openclaw-gateway sudo npm i -g openclaw@latest # ✅ 安装成功 sudo systemctl start openclaw-gateway
版本安装了,但你仔细想想——sudo npm i 的 HOME 是谁的?
默认 sudo 保留的 HOME 是 root 的 /root。当 OpenClaw 新版本网关第一次启动时,它做了一系列状态库迁移——全写到了 /root/.openclaw/。而 systemd 的 WorkingDirectory=/home/openclaw 只是工作目录,不决定状态库位置。
💡 关键洞察:OpenClaw 的状态库路径由 $HOME 环境变量决定,而不是 WorkingDirectory。systemd 中虽然配置了 WorkingDirectory=/home/openclaw,但并未设置 Environment=HOME=/home/openclaw(直到后续修复才加上)。因此新版启动时,$HOME 仍然是 systemd 默认的 / 或旧配置继承的值,造成配置库分裂。
此时的状态:
/root/.openclaw/— 新版生成的空配置(没有对话记录、没有技能、没有 approvals)/home/openclaw/.openclaw/— 旧版的完整配置(但新网关没在读它)
网关启动了,但是「裸奔」——没有任何配置,相当于一个全新的安装。没有模型配置,没有联系人,没有历史对话。
第三环:SSH 修复引入了 3097 个 root 文件
发现配置丢失后,SSH 进去手动修正:
# 强制指向正确的状态目录 export OPENCLAW_STATE_DIR=/home/openclaw/.openclaw # 重新启动——这一步实际上是 root shell 干的 sudo systemctl restart openclaw-gateway
但关键问题是——这些修复命令本身是在 root shell 里执行的。当新版网关在升级流程中写入新的状态文件时,owner 全是 root:
# 后续发现的受灾情况 $ find /home/openclaw/.openclaw/ -user root | wc -l 3097 # 问题文件分布 $ find /home/openclaw/.openclaw/ -user root -type f | head -10 /home/openclaw/.openclaw/exec-approvals.json # ← 最致命的 /home/openclaw/.openclaw/gateway.sqlite /home/openclaw/.openclaw/config.yaml /home/openclaw/.openclaw/state.json /home/openclaw/.openclaw/plugins/skill-manager/... /home/openclaw/.openclaw/sessions/...
升级完成了,网关工作了,但 3097 个文件埋着雷——等着在某个深夜引爆。
☠️ 死锁场景深度解析
这是整件事的高潮。大部分 root 文件虽然麻烦但不致命——真正杀死网关的是 exec-approvals.json。
exec-approvals.json 是什么?
OpenClaw 的 exec 工具在执行命令前会先检查 exec-approvals.json,这里记录着哪些命令被允许以什么权限执行。它是一个安全白名单。网关启动时会读取它,Agent 每次要执行命令时也要读它来校验权限。
死锁的形成
当 exec-approvals.json 的 owner 是 root 而网关进程以 openclaw 用户运行时:
这不是写入失败的小问题——这是读取级别的失败:
[ERROR] exec command rejected: EACCES: permission denied path: /home/openclaw/.openclaw/exec-approvals.json user: openclaw (uid=1001) action: read # Agent 的每次命令尝试都返回这个错误 # 包括"修复这个文件"所需要的 chown、cp、chmod 等命令
这就是真正的 死锁(Deadlock)——一个自指的矛盾锁:
你没法通过 Agent 执行命令来修复这个文件
因为执行命令需要读取这个文件
而读取这个文件需要先修复权限
死锁在日志中的表现
网关日志可以看到这个循环:
2026-07-16 19:33:12 [exec] Attempting to run: chown openclaw:openclaw exec-approvals.json 2026-07-16 19:33:12 [exec] Checking approvals from /home/openclaw/.openclaw/exec-approvals.json 2026-07-16 19:33:12 [exec] ERROR: EACCES: permission denied, open '/home/openclaw/.openclaw/exec-approvals.json' 2026-07-16 19:33:12 [approval] Command rejected: chown (no approvals file available) 2026-07-16 19:33:15 [exec] Attempting to run: cp /tmp/backup-exec-approvals.json ... 2026-07-16 19:33:15 [exec] ERROR: EACCES ... [same story] 2026-07-16 19:33:20 [system] Critical: 5 commands rejected in succession. Possible causes: approvals file missing, permissions broken, or disk full.
每一个 Agent 给出的修复方案都完美地撞死在同一个门上——你确实知道怎么办,但你没法让那个「怎么办」被执行。
✅ Step-by-Step 修复步骤
紧急修复:SSH 直连破局
死锁的唯一解是绕过执行管道——直接 SSH 到服务器,用 root shell 操作,不经过 Agent 和 approvals:
🔑 核心原则:当 exec-approvals.json 本身是受损文件时,任何通过 OpenClaw exec 管道发起的修复都会失败。必须从外部(SSH)操作。
# Step 1: SSH 登录
ssh root@your-server
# Step 2: 停网关(避免写入竞争)
systemctl stop openclaw-gateway
# Step 3: 修复 exec-approvals.json —— 这是最关键的一步
chown openclaw:openclaw /home/openclaw/.openclaw/exec-approvals.json
chmod 644 /home/openclaw/.openclaw/exec-approvals.json
# Step 4: 验证这步确保可读
sudo -u openclaw test -r /home/openclaw/.openclaw/exec-approvals.json && echo "✅ 可读"
# Step 5: 批量修复所有 root 文件
find /home/openclaw/.openclaw/ -user root -exec chown openclaw:openclaw {} \;
# Step 6: 验证已无 root 残留
find /home/openclaw/.openclaw/ -user root | wc -l
# 期望输出:0
# Step 7: 确认必要文件的权限
chmod -R u+rwX /home/openclaw/.openclaw/
# Step 8: 重启网关
systemctl start openclaw-gateway
# Step 9: 确认网关正常运行
systemctl status openclaw-gateway
journalctl -u openclaw-gateway --no-pager -n 20 | grep -i error
验证恢复
网关恢复后,在 WebChat 或任意终端确认 exec 功能已恢复:
# 在 Agent 对话中尝试一个无害的命令 执行: echo "permissions fixed" # 期望回应:命令正常执行,不再报 EACCES # 如果这一步通过,死锁已解除
🛡️ 如何避免再次发生
1. 固化环境变量到 systemd
最根本的修复——在 service 文件中显式锁定 $HOME:
[Service] User=openclaw Group=openclaw WorkingDirectory=/home/openclaw Environment=HOME=/home/openclaw # ← 显式锁定 HOME Environment=OPENCLAW_STATE_DIR=/home/openclaw/.openclaw # ← 可选,双重保险 Environment=PATH=/www/server/nodejs/v24.18.0/bin:/usr/local/bin:/usr/bin:/bin
加了 Environment=HOME=/home/openclaw 之后,无论怎样启动,状态库永远指向正确的路径。这是所有修复中最重要的一行。
2. 升级脚本模板
不要手敲升级命令。把这个脚本存在服务器上,每次升级跑一遍:
User=openclaw Group=openclaw 1
3. 权限审计钩子
作为日常巡检的一部分,每周或每次重启后检查一次:
User=openclaw Group=openclaw 2
⚙️ systemd 配置优化建议
基于这次血的教训,最终的 systemd 配置应该是这样——每行都有它的意义:
[Unit] Description=OpenClaw Gateway After=network-online.target Wants=network-online.target StartLimitBurst=5 StartLimitIntervalSec=60 [Service] Type=simple ExecStart=/www/server/nodejs/v24.18.0/bin/node /www/server/nodejs/v24.18.0/lib/node_modules/openclaw/dist/index.js gateway --port 18789 Restart=always RestartSec=5 RestartPreventExitStatus=78 TimeoutStopSec=30 TimeoutStartSec=30 SuccessExitStatus=0 143 KillMode=control-group # === 用户与权限 === User=openclaw Group=openclaw WorkingDirectory=/home/openclaw # === 环境变量(必须显式设置 === # HOME 决定状态库路径,不设置就可能是 /root 下生成空配置 Environment=HOME=/home/openclaw # OPENCLAW_STATE_DIR 是双重保险,覆盖所有可能的状态读取路径 Environment=OPENCLAW_STATE_DIR=/home/openclaw/.openclaw # PATH 确保能找到 npm、node 和其他工具 Environment=PATH=/www/server/nodejs/v24.18.0/bin:/usr/local/bin:/usr/bin:/bin # TMPDIR 确保临时文件写入可控目录 Environment=TMPDIR=/tmp # === 安全与清理 === # PrivateTmp 隔离 /tmp 访问,防止其他进程读取敏感数据 PrivateTmp=false # 关键:StateDirectory 告诉 systemd 这个目录是状态目录 # systemd 会自动创建并设置权限(需要 systemd 240+) StateDirectory=openclaw # 可选:限制能力集,不要给网关不必要的特权 CapabilityBoundingSet= NoNewPrivileges=true # 文件系统访问控制 ProtectSystem=strict ReadWritePaths=/home/openclaw [Install] WantedBy=multi-user.target
💡 配置要点解读:
Environment=HOME=/home/openclaw— 这是整个事故的「一票否决」行。没有它,任何依赖$HOME的路径都会飘到意想不到的地方。StateDirectory=openclaw— systemd 会管理这个目录的生命周期和权限,但前提是你的 systemd 版本 ≥ 240(OpenCloudOS 8/9 满足)。NoNewPrivileges=true— 防止进程通过 setuid 等方式提升权限,增加安全性。ProtectSystem=strict+ReadWritePaths— 严格限制可写范围,防止意外写扩散。
应用新配置
User=openclaw Group=openclaw 4
📝 最后的反思
回顾整个事件,真正的问题不是某个具体的 bug,而是一种最小权限原则的盲目区:
- npm 的全局安装需要 root,但运行时进程应该用普通用户——这是一个经典的「安装时特权 vs 运行时无特权」的矛盾。Node.js 社区对此没有好的原生解法。
- sudo 保留了 root 的 HOME,但没有人提醒你——这是 sudo 的行为惯性,不是 OpenClaw 的设计失误。
- exec-approvals.json 变成了自己的守卫——任何用它来保护自身修复的尝试都是不可能的。这是一个不该存在的自指循环。如果未来 OpenClaw 能对这个文件做特例处理(比如 fallback 到默认批准的维护命令集),或者提供一个「维护模式」入口绕过 approvals 校验,这个死锁就不会发生。
对于自建 OpenClaw 的用户,我的建议很简单:
- 升级前 — 打开
/etc/systemd/system/openclaw-gateway.service,确认Environment=HOME=/home/你的用户名这一行存在且正确。 - 升级后 — 跑一遍
find /home/你的用户名/.openclaw/ -user root | wc -l,确认数字为 0。 - 日常 — 把审计脚本放到 cron 里,每周跑一次。
这两个小时的血泪,最终就换来了那三行 systemd 配置、一个审计脚本、和一个永远不会忘记的经验:权限问题不会自己消失,它只是等着在凌晨两点给你一个惊喜。
2026年7月 · 可亓



黑公网安备 23010302001359号