systemd-run --working-directory 的 ~ 不展开:一次 status=200/CHDIR 排查

十月 09, 2026 [Linux 与系统] #systemd #调试 #Shell

现象

一条很普通的 systemd-run --user 后台服务命令:

systemd-run --user --working-directory=~/project/agent-code/ --unit opencode \
  --setenv OPENCODE_SERVER_PASSWORD="<密码>" \
  ~/.opencode/bin/opencode serve --hostname 192.168.5.68 --port 4096

systemd-run 本身不报任何错,正常打印 Running as unit: opencode.service,但服务立刻挂掉。

排查

$ systemctl --user status opencode --no-pager -l
× opencode.service - [systemd-run] /home/peter/.opencode/bin/opencode serve ...
     Loaded: loaded (/run/user/1000/systemd/transient/opencode.service; transient)
  Transient: yes
     Active: failed (Result: exit-code) since Fri 2026-10-09 12:01:35 CST; 2min 15s ago
   Duration: 32ms
    Process: 3358420 ExecStart=/home/peter/.opencode/bin/opencode serve ... (code=exited, status=200/CHDIR)
  Main PID: 3358420 (code=exited, status=200/CHDIR)

10月 09 12:01:35 debian13 (opencode)[3358420]: opencode.service: Changing to the requested working directory failed: No such file or directory
10月 09 12:01:35 debian13 (opencode)[3358420]: opencode.service: Failed at step CHDIR spawning /home/peter/.opencode/bin/opencode: No such file or directory

两个疑点:

  1. 目录明明存在:ls -ld ~/project/agent-code/ 输出正常,drwxrwxr-x 权限也没问题;
  2. 同一条命令里,二进制路径 ~/.opencode/bin/opencode 显然展开成功了——unit 描述里已经是 /home/peter/.opencode/bin/opencode,偏偏 --working-directory= 后面的没展开。

原因:~ 的展开规则比想象中苛刻

shell 对 ~ 的 tilde expansion 只发生在两种位置:

而 --working-directory=~/project/agent-code/ 这个单词以 -- 开头,= 前面是 -,不是合法的变量名,shell 不认为它是赋值样式单词,~ 原样透传。systemd 拿到字面量 ~/project/agent-code/,而 systemd 侧(包括 unit 文件)从不做任何 ~ 展开,CHDIR 一个叫 ~ 的目录自然 ENOENT。

四个 shell 实测矩阵(2026-10,bash 5.2 / zsh 5.9 / dash 0.5.12):

写法bashzshsh/dash
~/x(词首)✅ 展开✅ 展开✅ 展开
--opt=~/x(命令参数)❌ 不展开❌ 不展开❌ 不展开
VAR=~/x(命令参数)✅ 展开❌ 不展开❌ 不展开
真赋值 X=~/x / 前缀赋值✅ 展开✅ 展开✅ 展开

第二、三行是容易踩的组合坑:bash 会把「长得像赋值」的参数做 tilde 展开(echo VAR=~/bin 能展开),让人误以为 = 后面的 ~ 都能展开;但 --opt=~/x 因为 = 前带连字符不属于合法标识符,任何 shell 都不展开。别赌 shell 行为,这类位置一律别用 ~。

顺带解读一下 status=200/CHDIR:systemd 把 200+ 的退出码留给自身 exec 流程各步骤的失败标记(200=CHDIR,201=NICE,202=FDS,203=EXEC……),最出名的是 203/EXEC——shell 脚本没写 shebang 时报的那个。看到 200/CHDIR 就直接指向 WorkingDirectory 不可达,与程序本身无关。

解决

推荐直接用绝对路径:

systemd-run --user --working-directory=/home/peter/project/agent-code --unit opencode \
  --setenv OPENCODE_SERVER_PASSWORD="<密码>" \
  ~/.opencode/bin/opencode serve --hostname 192.168.5.68 --port 4096

不想写死用户名可以用 $HOME(参数展开发生在单词任意位置,不受 tilde 规则限制):

systemd-run --user --working-directory="$HOME/project/agent-code" --unit opencode ...

附坑:失败的 transient unit 占用 unit 名

修复后直接重跑 systemd-run --unit opencode ... 会再吃一记:

$ systemd-run --user --unit opencode ...
Failed to start transient service unit: Unit opencode.service was already loaded or has a fragment file.

失败的 transient unit 仍占着名字,先清掉再跑:

systemctl --user reset-failed opencode.service

总结