
昨天 GitHub 趋势榜的第一名是一个求职工具,单日 3890 星,是第二名的 2.5 倍。它的做法值得看看:不是又一个聊天页面,而是把 Claude Code 当成运行时,把「评估岗位、改简历、写求职信、备面试」串成一条流水线。榜单上 agent 周边项目占了大半,调度器、记忆层、技能包、多 CLI 切换器,做的是同一件事:让 agent 从「你一句我一句」变成可以被程序调用的组件。
这件事技术上早就成立,只是多数人从没打开过那个开关。Claude Code 有 -p,Codex 有 codex exec,opencode 干脆把自己拆成服务端加客户端。本文把三家的非交互接口摊开看,重点放在文档里一笔带过、真接进流水线才会咬人的地方。
聊天框是最难集成的接口
要在 CI 里跑一个 agent,你需要的是退出码、结构化输出、超时控制、日志,以及「重跑一次结果一样」。聊天框这四样全都没有:它给人读,不给你读。
我自己博客的日更任务就是 cron 起来之后,让 agent 在 shell 里跑完写稿、生成封面、提交推送。整个过程没人在旁边点确认。能这么干,靠的不是模型多聪明,是那条命令把「过程」和「结果」分开了。
Codex:先把 stderr 和 stdout 分开
codex exec "summarize the repository structure and list the top 5 risky areas"
跑的过程中,进度写 stderr,只有最终的 agent 消息进 stdout。这个设计看着不起眼,但决定了你能直接 | tee release-notes.md,不用写解析器剥掉一堆进度日志。
默认沙箱是 read-only,所以「能想不能改」。要改文件得显式放开:
codex exec --sandbox workspace-write "fix the failing test"
danger-full-access 会同时放开编辑和联网,官方建议只在隔离的 runner 或容器里用。这条不是客套,跑过无人值守任务的人都知道,放开网络之后 agent 会去装依赖、拉包、改系统包,一次任务能把环境弄成什么样完全靠运气。
真正把 agent 变成组件的是 --json:
codex exec --json "summarize the repo structure" | jq
stdout 变成 JSONL 流,事件类型有 thread.started、turn.started、item.started/updated/completed、turn.completed、turn.failed、error。item 覆盖命令执行(带 aggregated_output 和 exit_code)、文件改动、MCP 工具调用、联网搜索、思考摘要。turn.completed 里带 usage,含输入、缓存命中、输出和推理的 token 数。
这套事件流的价值在于,你不用再从一段自然语言里猜「它到底执行了什么」。哪条命令失败了、退出码多少、改了哪些文件,都是字段,可以直接入库。
结构化输出走 --output-schema,配严格的 JSON Schema:
codex exec "Extract project metadata" --output-schema ./schema.json -o ./meta.json
-o 把最终消息单独落盘,stdout 照样打印。多阶段流水线用 codex exec resume --last "fix the race conditions you found" 接上一轮上下文;不想留会话文件加 --ephemeral。stdin 也能用:提示词当指令,管道进来的内容当上下文。
Claude Code:复杂的地方全在权限
claude -p "Find and fix the bug in auth.py" --allowedTools "Read,Edit,Bash"
最基本的约定挺好用:成功退出码 0,失败非零,脚本可以分支。但有个反直觉的点,运行内部失败(比如认证缺失)不是靠退出码报的,而是把失败当作 result 打在 stdout 上。所以别把退出码当唯一判断依据。
输出格式三档:text 默认,json 带 result、session_id、total_cost_usd,stream-json 是按行的事件流。要固定结构就上 --json-schema:
claude -p "Extract the main function names from auth.py" \
--output-format json \
--json-schema '{"type":"object","properties":{"functions":{"type":"array","items":{"type":"string"}}},"required":["functions"]}'
结果是放在 structured_output 字段里的。坑在下一句:如果这一轮没产出合法结构,比如模型直接拒绝了,响应里根本不会有 structured_output。官方文档专门提醒要先判字段存在再取值。这类「拒绝不等于报错,只是少了一个字段」的失败模式,写脚本时最容易漏。
权限是另一回事。--allowedTools 用的是权限规则语法,支持前缀匹配:
claude -p "Look at my staged changes and create an appropriate commit" \
--allowedTools "Bash(git diff *),Bash(git log *),Bash(git status *),Bash(git commit *)"
星号前面那个空格是必须的。写成 Bash(git diff*),git diff-index 也会被放行。这种差别不写在显眼位置,但它是「只允许读 diff」和「允许跑一堆 git 子命令」之间的距离。
无人值守时还有两层开关。--permission-mode 定基线:auto 让分类器审每个动作,dontAsk 把凡是需要问的直接拒掉,适合锁死的 CI,acceptEdits 自动放行写文件和 mkdir、touch、mv、cp 这类常见操作。注意默认值跟版本有关,不同版本行为不一样,显式传比赌默认值稳。
--permission-prompts none(需要 v2.1.259 以上)是给「确定没人能答」的场合用的:它把需要人回答的工具直接摘掉,比如 AskUserQuestion,被拒的调用在 stream-json 里以 permission_denied 系统消息出现,最后汇总在 permission_denials 里。
还有一个容易被忽略的安全项:--bare。它会跳过 hooks、技能、自定义命令、subagent、已装插件、MCP 服务器、自动记忆和 CLAUDE.md 的自动发现。文档里那句「不加的话,某个同事 ~/.claude 里的 hook、项目 .mcp.json 里的 server 都会在这个会话里跑起来,而 -p 会话没有工作区信任弹窗」值得读两遍。它既是可复现性问题,也是攻击面问题:仓库里的配置可以趁着无人值守时执行东西。代价是 bare 模式不认订阅登录,得准备 API Key。
后台任务有个时间陷阱。claude -p 主对话结束后,如果还有后台命令或 subagent 在跑,它会等;默认连续空闲 10 分钟就停掉并丢弃部分结果,CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS 可以调这个上限。所以超时该设在进程外面,不要指望 agent 自己收敛。
opencode:TUI 只是服务端的一个客户端
opencode 的架构跟上面两家不同。你跑 opencode 时,它同时起了一个 TUI 和一个服务端,TUI 只是连过去的客户端。想纯 headless 就:
opencode serve --port 4096 --hostname 127.0.0.1
服务端在 /doc 暴露 OpenAPI 3.1 规范,SDK 就是拿这份规范生成的,所以类型是跟着服务端走的,不会漂。事件用 SSE,/global/event 给全局流,/event 给单实例流,第一个事件是 server.connected。会话、消息、provider 授权、OAuth 回调全在 HTTP 层,拿到 session id 就能跨进程接着聊。
反过来也有意思:/tui/* 这组端点可以驱动 TUI,预填提示词、提交、执行命令。IDE 插件就是这么做的,它在协议上和人类的键盘没有区别。
部署形态上它准备得最齐:OPENCODE_SERVER_PASSWORD 开 basic auth(默认用户名 opencode),--cors 放行指定来源,opencode pair 发一次性登录链接和二维码(5 分钟过期、只能用一个),opencode service 管后台服务,opencode api 直接对着运行中的服务发请求。
代价是状态不在进程里。服务一挂,会话就悬在半空;多客户端并发打同一个 session 时,谁的状态算数得你自己定规矩。
三家放一起看
| 维度 | Codex | Claude Code | opencode |
|---|---|---|---|
| 入口 | codex exec | claude -p | opencode serve + SDK |
| 过程与结果 | stderr 过程,stdout 结果 | 都走同一路输出,按格式切 | HTTP + SSE |
| 机器可读 | JSONL 事件,含 usage | json / stream-json,含 cost | OpenAPI 生成的类型 |
| 结构化 | --output-schema 严格 schema | --json-schema 到 structured_output | 由客户端定义 |
| 权限 | 沙箱三档,按能力切 | 工具级规则 + permission mode | 服务端密码 + provider 授权 |
| 续跑 | resume --last / 指定 session | --continue / session id | 会话在服务端 |
差异几乎都能归到一件事:谁来批准工具调用。Codex 用沙箱档位,粗粒度,按文件系统和网络的权限切;Claude Code 用工具级规则,细到命令前缀;opencode 把决定权交给调用方,HTTP 客户端想怎么批就怎么批。
这也解释了为什么榜单上「多 CLI 切换器」能拿到 500 多星。用户要的不是某个模型,是一个能同时管住几家、按预算挑、失败就换的调度层。而调度层要做的第一件事,就是把这三种完全不同的权限模型翻译成一套。
接进流水线前先立几条规矩
- 退出码和结构化输出一起用。只看退出码会漏掉「内部失败写在 stdout」这类情况,只看输出会漏掉「进程根本没起来」。
- 结构化字段必须判空。模型的拒绝常常表现为少一个字段,不是一个错误。
- 超时设在进程外。
timeout命令、CI job 上限,都行,别指望 agent 内部那套空闲计时。 - stdout 只留结果,过程进 stderr 或日志文件。混在一起之后,你迟早要写一个永远修不干净的解析器。
- 从最小沙箱起步。
danger-full-access配一次性容器,别配开发机。 - CI 里做隔离,等价于
--bare那种思路:别让宿主机的个人配置悄悄改变任务行为。 - 幂等。同一个任务重跑要能收敛,别出现 agent 提交两次、发两条通知的情况。
最后
昨天那个榜单给的信号很清楚:大家不再比谁的聊天界面漂亮,而在比谁能让 agent 跑起来、跑得住、跑得起。榜单上的记忆层、切换器、调度器,做的都是同一件配套活。
我自己的判断是,接下来的分水岭不在模型智商上那 5% 的差距,而在非交互接口是否稳定、权限是否可编排、成本是否可预测。反过来说,榜单上那些薄壳也最脆弱:上游改一次 CLI 的默认行为,或者收紧一次订阅策略,整条产品线当天就得返工。把 agent 当运行时是好事,但要记得运行时是别人的。