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
两个疑点:
- 目录明明存在:
ls -ld ~/project/agent-code/输出正常,drwxrwxr-x权限也没问题; - 同一条命令里,二进制路径
~/.opencode/bin/opencode显然展开成功了——unit 描述里已经是/home/peter/.opencode/bin/opencode,偏偏--working-directory=后面的没展开。
原因:~ 的展开规则比想象中苛刻
shell 对 ~ 的 tilde expansion 只发生在两种位置:
- 单词开头(
~/.opencode/bin/opencode)——所以二进制路径展开成功; - 真正的赋值上下文(
X=~/x、export X=~/x、命令前缀赋值X=~/x cmd)。
而 --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):
| 写法 | bash | zsh | sh/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
总结
systemd-run报status=200/CHDIR时第一件事:systemctl --user status看日志里实际收到的WorkingDirectory路径,~没展开是最常见原因;- shell 的 tilde expansion 只认「词首」和「真赋值上下文」,
--长选项=~/x在所有主流 shell 都不展开——给程序传的路径参数一律用绝对路径或$HOME; - transient unit 失败后名字会被占用,
reset-failed清理后再重跑。