macOS LaunchAgent 开机自启服务配置实战:以 OpenClaw 为例
title: “macOS LaunchAgent 开机自启服务配置实战:以 OpenClaw 为例”
tags:
- macOS
- LaunchAgent
- 开机自启
- launchd
- OpenClaw
categories: - macOS
description: “从原理到实战,详解 macOS LaunchAgent 的配置方法,以 OpenClaw Gateway 和 CLIProxyAPI 为例,手把手教你实现服务的开机自启和崩溃自动重启。”
导读:macOS 上想让自己的服务开机自启,很多人第一反应是往"系统设置 → 登录项"里塞,结果弹个终端窗口出来,丑且不靠谱。正确的做法是用 macOS 原生的 launchd 机制,通过 LaunchAgent plist 文件来管理。这篇文章我把自己折腾 LaunchAgent 的经验整理出来,用 OpenClaw 和 CLIProxyAPI 两个真实案例走一遍完整流程。
一、先搞清楚 launchd 是什么
launchd 是 macOS 的服务管理器,系统启动时由内核第一个拉起来(PID 1),负责管理所有系统服务和用户服务。你可以把它理解为 macOS 版的 systemd。
launchd 通过读取 .plist 配置文件来决定启动什么、什么时候启动、崩了要不要拉起来。
LaunchAgent vs LaunchDaemon
这两个东西经常搞混,其实区别很简单:
| LaunchAgent | LaunchDaemon | |
|---|---|---|
| 谁启动 | 用户登录后启动 | 系统启动时(不需要登录) |
| 权限 | 当前用户权限 | root 权限 |
| 能弹 GUI 吗 | 能 | 不能 |
| 配置目录 | ~/Library/LaunchAgents | /Library/LaunchDaemons |
日常开发中我们要自启的服务(代理、数据库、开发工具后台服务等),99% 用 LaunchAgent 就够了。
plist 文件放哪
~/Library/LaunchAgents/ ← 用户级,登录后启动(最常用)
/Library/LaunchAgents/ ← 系统级,所有用户登录后启动(需要 sudo)
/Library/LaunchDaemons/ ← 系统级,开机就启动(需要 sudo + root)
/System/Library/LaunchAgents/ ← 苹果系统自带,别动
/System/Library/LaunchDaemons/ ← 苹果系统自带,别动
我们写自己的服务,放到 ~/Library/LaunchAgents/ 就行,不需要 sudo,不需要 root。
二、plist 配置文件详解
一个典型的 LaunchAgent plist 文件长这样:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.example.myservice</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/myservice</string>
<string>--config</string>
<string>/path/to/config.yaml</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/myservice.log</string>
<key>StandardErrorPath</key>
<string>/tmp/myservice.err.log</string>
</dict>
</plist>
关键配置项说明
| 配置项 | 作用 | 常用值 |
|---|---|---|
Label | 服务唯一标识,必须和文件名一致 | 反域名格式,如 com.example.myservice |
ProgramArguments | 启动命令和参数,数组的第一个是可执行文件路径 | 数组形式 |
RunAtLoad | 加载时是否立即启动 | true / false |
KeepAlive | 进程退出后是否自动重启 | true / false / 字典条件 |
StandardOutPath | 标准输出日志路径 | 绝对路径 |
StandardErrorPath | 错误输出日志路径 | 绝对路径 |
WorkingDirectory | 工作目录 | 绝对路径 |
EnvironmentVariables | 环境变量 | 字典形式 |
StartInterval | 定时执行间隔(秒) | 整数 |
WatchPaths | 监控路径,文件变化时触发 | 数组 |
ThrottleInterval | 重启最小间隔(秒),防频繁重启 | 整数,默认 10 |
KeepAlive 的几种写法
KeepAlive 可以是简单的 true,也可以是条件式的:
<!-- 无条件自动重启 -->
<key>KeepAlive</key>
<true/>
<!-- 仅在退出码非 0 时重启 -->
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
</dict>
<!-- 网络可用时才保持运行 -->
<key>KeepAlive</key>
<dict>
<key>NetworkState</key>
<true/>
</dict>
我的建议:大多数场景直接设 true 就完事了,除非你明确知道进程正常退出后不该重启。
三、实战案例一:OpenClaw Gateway 自启
OpenClaw 是一个开源的个人 AI 助手,可以在微信、Telegram、Discord 等平台上运行。它需要一个 Gateway 服务常驻后台。
好消息是,OpenClaw 安装时自带了 daemon 管理,一条命令就能搞定:
openclaw onboard --install-daemon
执行后它会自动在 ~/Library/LaunchAgents/ 下生成一个 ai.openclaw.gateway.plist 文件。我们来拆解一下这个真实的 plist 配置:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>ai.openclaw.gateway</string>
<key>Comment</key>
<string>OpenClaw Gateway (v2026.4.9)</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>ThrottleInterval</key>
<integer>1</integer>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/opt/node/bin/node</string>
<string>/opt/homebrew/lib/node_modules/openclaw/dist/entry.js</string>
<string>gateway</string>
<string>--port</string>
<string>18789</string>
</array>
<key>StandardOutPath</key>
<string>/Users/aiksyuan/.openclaw/logs/gateway.log</string>
<key>StandardErrorPath</key>
<string>/Users/aiksyuan/.openclaw/logs/gateway.err.log</string>
<key>EnvironmentVariables</key>
<dict>
<key>HOME</key>
<string>/Users/aiksyuan</string>
<key>PATH</key>
<string>/opt/homebrew/opt/node/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
<key>OPENCLAW_GATEWAY_PORT</key>
<string>18789</string>
</dict>
</dict>
</plist>
这个配置有几个值得注意的点:
ProgramArguments用了 node 的完整路径(/opt/homebrew/opt/node/bin/node),而不是直接写node。这是因为 launchd 不会加载你的 shell 环境,PATH 变量可能不包含 node。EnvironmentVariables里手动设了 PATH 和 HOME,确保进程能找到需要的命令。ThrottleInterval设为 1,允许崩溃后快速重启(默认是 10 秒)。
💡 这是 OpenClaw 自动生成的配置,不需要手动写。如果你也是 OpenClaw 用户,直接跑
openclaw onboard --install-daemon就行了。
四、实战案例二:CLIProxyAPI 自启(手动配置)
CLIProxyAPI 是一个 API 协议转换代理,用于让 Codex CLI 等工具接入智谱 GLM 模型。它没有自带的 daemon 管理功能,需要我们自己写 plist。
4.1 创建 plist 文件
vim ~/Library/LaunchAgents/com.cli-proxy-api.plist
写入以下内容(记得把 aiksyuan 换成你自己的用户名):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.cli-proxy-api</string>
<key>ProgramArguments</key>
<array>
<string>/Users/aiksyuan/.codex/cli-proxy-api</string>
<string>-config</string>
<string>/Users/aiksyuan/.codex/cliproxy-config.yaml</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/aiksyuan/.codex/logs/cli-proxy-api.stdout.log</string>
<key>StandardErrorPath</key>
<string>/Users/aiksyuan/.codex/logs/cli-proxy-api.stderr.log</string>
</dict>
</plist>
这里几个要点:
Label是com.cli-proxy-api,文件名也是com.cli-proxy-api.plist,保持一致。KeepAlive设true,代理崩了会自动重启,不会出现 Codex CLI 连不上的情况。- 日志写到
~/.codex/logs/,方便排查问题。
4.2 加载服务
# 加载并立即启动
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.cli-proxy-api.plist
⚠️ 如果你之前用的是旧版
launchctl load命令,macOS 10.10 之后推荐用launchctl bootstrap替代。两者的区别:bootstrap是基于 domain 的操作,语义更清晰。
4.3 验证服务
# 检查服务状态
launchctl print gui/$(id -u)/com.cli-proxy-api
# 测试代理是否正常响应
curl -s -H "Authorization: Bearer sk-glm-proxy" http://127.0.0.1:8080/v1/models
正常的话应该返回模型列表。
五、launchctl 常用命令速查
这些命令我日常用得很多,建议收藏:
# 加载服务(立即启动 + 注册开机自启)
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.plist
# 卸载服务(停止 + 取消开机自启)
launchctl bootout gui/$(id -u)/com.example
# 查看服务状态
launchctl print gui/$(id -u)/com.example
# 列出当前用户所有已加载的服务
launchctl list
# 列出并过滤特定服务
launchctl list | grep openclaw
# 手动启动服务(需要已加载)
launchctl kickstart gui/$(id -u)/com.example
# 停止服务(不卸载,崩溃后会自动重启如果 KeepAlive=true)
launchctl kill SIGTERM gui/$(id -u)/com.example
💡
gui/$(id -u)是当前用户的 launchd domain。$(id -u)获取你的用户 UID(通常是 501)。你也可以直接写gui/501。
六、我踩过的几个坑
坑 1:launchd 环境里没有你的 PATH
这是最多人踩的坑。launchd 不会读 .zshrc、.bash_profile 这些文件,所以你在 shell 里能跑的命令,放到 plist 里可能找不到。
解决方案:在 ProgramArguments 里写可执行文件的绝对路径,或者通过 EnvironmentVariables 手动设 PATH。
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
</dict>
坑 2:plist 文件权限不对
plist 文件必须是 644(所有者可读写,其他人只读),且所有者必须是当前用户。如果权限不对,launchd 会拒绝加载。
chmod 644 ~/Library/LaunchAgents/com.example.plist
坑 3:改了 plist 不生效
修改 plist 文件后,必须先 bootout 再 bootstrap,否则 launchd 还是用旧的配置:
launchctl bootout gui/$(id -u)/com.example
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.plist
坑 4:日志目录不存在
如果 StandardOutPath 或 StandardErrorPath 指向的目录不存在,launchd 会静默失败,服务起不来但也不报错。确保日志目录已经创建好:
mkdir -p ~/.codex/logs
mkdir -p ~/.openclaw/logs
坑 5:Label 和文件名不一致
Label 的值不要求和文件名完全一致,但这是约定俗成的最佳实践。如果 Label 冲突(两个 plist 用了同一个 Label),后加载的会覆盖前一个。
七、用 Shell 脚本包装一下更省心
如果你的服务启动前需要一些环境准备工作(比如检查端口、创建目录),可以把启动逻辑包在 Shell 脚本里,然后在 plist 中调用脚本:
#!/bin/bash
# ~/scripts/start-cli-proxy.sh
LOG_DIR="$HOME/.codex/logs"
mkdir -p "$LOG_DIR"
if lsof -i :8080 >/dev/null 2>&1; then
echo "Port 8080 already in use, skipping"
exit 0
fi
exec "$HOME/.codex/cli-proxy-api" -config "$HOME/.codex/cliproxy-config.yaml"
plist 中改成:
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/Users/aiksyuan/scripts/start-cli-proxy.sh</string>
</array>
⚠️ 脚本必须有执行权限:
chmod +x ~/scripts/start-cli-proxy.sh
八、定时任务也能用 LaunchAgent
除了开机自启,LaunchAgent 还能做定时任务。比如每天凌晨 3 点清理日志:
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>3</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
或者每隔 5 分钟执行一次:
<key>StartInterval</key>
<integer>300</integer>
我之前用 cron 做定时任务,后来发现 macOS 对 cron 的支持越来越弱,权限还有限制。现在定时任务我全部用 LaunchAgent 来做,更原生的方案。
九、和"系统设置 → 登录项"的区别
很多人可能在"系统设置 → 通用 → 登录项"里添加过启动程序。这两者的区别:
| 登录项 | LaunchAgent | |
|---|---|---|
| 能跑后台服务吗 | 勉强能,但会弹窗口 | 天生为后台服务设计 |
| 崩溃自动重启 | 不能 | KeepAlive 支持 |
| 日志管理 | 没有 | StandardOutPath |
| 定时任务 | 不支持 | StartInterval / StartCalendarInterval |
| 环境变量控制 | 继承用户环境 | 手动指定,更可控 |
| 适合场景 | GUI 应用(比如 iTerm、浏览器) | 后台服务、脚本、代理 |
简单说:GUI 应用用登录项,后台服务用 LaunchAgent。
十、常见问题
Q:launchctl bootstrap 报 5: Input/output error 怎么回事?
多半是服务已经加载过了。先 bootout 再 bootstrap:
launchctl bootout gui/$(id -u)/com.example
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.plist
Q:服务状态显示 exit code 1,但日志里什么都没有?
检查一下可执行文件的路径是不是对的,用绝对路径。另外看日志目录是否存在。
Q:怎么查看服务为什么崩了?
看 StandardErrorPath 指定的错误日志。另外 launchctl print gui/$(id -u)/com.example 里会显示最后一次退出状态。
Q:能不能不重启就测试 plist?
可以。bootstrap 加载后立即启动,不需要重启电脑。bootout 后服务立即停止。
Q:卸载服务要不要删 plist 文件?
不删也行,先 bootout 停掉就行。但如果彻底不用了,建议 bootout 后删掉 plist 文件,保持 ~/Library/LaunchAgents/ 干净。
参考链接
更多推荐



所有评论(0)