Claude Code、Codex、Gemini CLI、Copilot CLI、OpenCode 的 Headless 模式與 CI 設定指南
Headless Mode in 5 Agents: claude -p, codex exec, gemini -p, copilot -p, opencode run
作者根據各廠商現行文件,整理 Claude Code、Codex、Gemini CLI、Copilot CLI 和 OpenCode 五款 Agent CLI 的無人值守命令、權限設定、輸出格式、工作階段續接與退出碼。
- Book: System Design Pocket Guide: Fundamentals
- The series: Pocket Guides for Developers — 8 short books on agents, RAG, prompts, observability, events, databases and system design — the whole series
- My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools
- Me: xgabriel.com | GitHub
A claude -p run in CI reaches a tool call that needs approval. Nobody is there to give it, so the call is denied and the run carries on, according to the Claude Code headless docs. The agent can still finish with a message saying it fixed the flaky test, and the job goes green on an empty diff.
Gemini CLI does the same on purpose: a rule that would ask you "is treated as deny" in non-interactive mode.
Every agent CLI has its own version of this trap. Here are five, from the vendors' current docs, linked where each flag appears. I haven't run these in a pipeline myself, so check --help on the version you pin.
The cheat sheet
| Agent | Headless command | Machine output | Pre-approve | Resume |
|---|---|---|---|---|
| Claude Code | claude -p "..." |
--output-format json or stream-json
|
--allowedTools, --permission-mode
|
--continue, --resume <id>
|
| Codex | codex exec "..." |
--json (JSONL), -o <file>
|
--sandbox workspace-write |
codex exec resume --last or <id>
|
| Gemini CLI | gemini -p "..." |
--output-format json or stream-json
|
--approval-mode, policy rules |
--resume latest |
| Copilot CLI | copilot -p "..." |
--output-format json (JSONL), -s
|
--allow-tool, --deny-tool
|
--continue, --resume=<id>
|
| OpenCode | opencode run "..." |
--format json |
permission in opencode.json, --auto
|
-c, -s <id>
|
Gemini CLI needs a caveat. Its own docs say it "was replaced by Antigravity CLI on June 18th, 2026" for unpaid-tier and Google One users (Gemini CLI headless reference). If your account still runs Gemini CLI, the row holds. If it moved, the successor's print mode is agy -p: it gained machine-readable output through --output-format, plus --json-schema, in Antigravity CLI 1.1.8. Print-mode usage errors exit with status 2; 1.3.4 fixed the last one that still exited 0.
For Copilot, the command reference documents --continue and --resume as general flags. It doesn't spell out how they combine with -p, so try a resume once by hand before a pipeline depends on it.
Permissions: where headless runs fail quietly

Five agents, five permission models. Some of them deny without telling you.
Claude Code
Pass a mode explicitly. A run where nothing sets one takes Claude Code's built-in starting mode, which "can be auto" (headless docs). acceptEdits lets it write files, but shell commands outside the read-only set still need an --allowedTools entry.
In CI, add --bare. Without it, a -p session runs the hooks in the repo's .claude/settings.json and connects the servers in its .mcp.json, with no trust dialog. Bare mode skips them, and it reads no subscription login, so set ANTHROPIC_API_KEY.
claude --bare -p "Fix the failing test in auth" \
--permission-mode acceptEdits \
--allowedTools "Bash(npm test *)" \
--output-format json > result.json
jq -e '.permission_denials | length == 0' result.json
Bash(npm test *) also matches plain npm test (permission rules). The jq -e line fails the job when anything was denied: both the success and the error shape of the result message carry a permission_denials array (SDK types).
Codex
codex exec "runs in a read-only sandbox" by default (non-interactive docs). Ask it to fix a bug without --sandbox workspace-write and it can read your repo but not change it. The old --full-auto flag "is no longer supported". Use --sandbox workspace-write, or --approve-for-me to send approval requests through automatic review. Keep danger-full-access for a controlled environment.
Codex also expects a Git repository; --skip-git-repo-check overrides that. The same page warns against setting OPENAI_API_KEY or CODEX_API_KEY as a job-level variable in workflows that run repository-controlled code.
Gemini CLI
--approval-mode takes default, auto_edit, yolo or plan. --yolo and --allowed-tools are both deprecated, the second in favour of the policy engine (CLI reference). A user policy in ~/.gemini/policies/ can allow one command, only when no terminal is attached:
# ~/.gemini/policies/ci.toml
[[rule]]
toolName = "run_shell_command"
commandPrefix = "npm test"
decision = "allow"
priority = 100
interactive = false
interactive = false makes the rule apply "only in non-interactive (headless) mode" (policy engine), so your interactive sessions still ask.
Copilot CLI
Copilot's programmatic reference is silent on what -p does with a tool you never allowed. List what it may run and what it may not. Deny rules win "even when --allow-all is set" (allowing tools), and --allow-all (alias --yolo) equals --allow-all-tools --allow-all-paths --allow-all-urls.
copilot -p "Fix the failing test in auth" -s \
--allow-tool='write, shell(npm:*)' \
--deny-tool='shell(npm publish)' \
--no-ask-user
--no-ask-user stops it pausing to ask clarifying questions. One more trap from the programmatic how-to: if you pipe input in and also pass -p, the piped input is ignored.
OpenCode
OpenCode is the inverse of the other four. "Most permissions default to allow"; only doom_loop and external_directory default to ask, and .env reads are denied (permissions docs). So opencode run can edit files and run shell commands with no flag at all. Your guard is a deny rule in the project's opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"permission": {
"bash": {
"*": "ask",
"git push *": "deny",
"rm *": "deny"
}
}
}
opencode run --auto "Fix the failing test in auth"
--auto approves every request "not explicitly denied", so each ask becomes a yes and the two deny lines are the whole fence. Without --auto, a headless ask is undocumented; treat it as unknown. And -p on opencode run means --password, the server's basic-auth password, not the prompt. The prompt is the positional argument.
Output: keep progress out of your result
The second trap is parsing a log when you wanted an answer.
-
Claude Code prints one JSON object with
--output-format json: the text inresult, plussession_idandtotal_cost_usd, a client-side estimate. Add--json-schemaand the parsed answer lands instructured_output. -
Codex is clean by default: "progress to
stderr" and "only the final agent message tostdout".--jsonturns stdout into JSONL, fromthread.started(with athread_id) toturn.completedorturn.failed.-owrites the final message to a file. -
Gemini CLI returns
response,statsand an optionalerrorobject in JSON mode (headless reference). -
Copilot CLI adds stats and decoration unless you pass
-s, which outputs "only the agent's response".--output-format jsonemits JSONL. -
OpenCode emits "raw JSON events" with
--format json. The CLI page doesn't describe their structure, so pin a version before you parse them.
The session ID is how you chain runs. Both of these come straight from the docs:
id=$(claude -p "Start a review" --output-format json \
| jq -r '.session_id')
claude -p "Continue that review" --resume "$id"
codex exec "review the change for race conditions"
codex exec resume --last "fix the race conditions you found"
Exit codes
Only two of the five document their exit codes for headless runs.
-
Claude Code: 0 on success, non-zero when the run fails. A failure inside the run, such as missing auth, is printed as the result on stdout. SIGTERM gives 143. In JSON,
is_errorand asubtypesuch aserror_max_turnssay why. - Gemini CLI: 0 success, 1 general or API error, 42 input error, 53 turn limit exceeded (headless reference).
-
Codex: the non-interactive page names one failure exit, a
required = trueMCP server that fails to start. For everything else, read the events. -
Copilot CLI: the reference gives exit codes for
copilot workflow run, not for-p. -
OpenCode: the CLI page says nothing about exit codes for
run.
Exit 0 only means the process ended, so add two checks of your own after an agent step. For Codex, fail on a failed turn. For any agent step that is supposed to change files, fail when it changed nothing:
jq -s -e 'all(.[]; .type != "turn.failed")' events.jsonl
if [ -z "$(git status --porcelain)" ]; then
echo "agent changed nothing" >&2
exit 1
fi
The first line assumes you saved codex exec --json output to events.jsonl. The second check catches an empty-diff green build, whichever agent ran.
When a human needs to be in the loop
Headless mode is for the runs nobody watches. The rest of the time you want the same agents with their prompts on screen.
I build Hermes IDE, so weigh this with that in mind. It's a desktop app, not a CI runner. It runs Claude Code, Codex, Antigravity CLI, Gemini CLI, GitHub Copilot CLI, OpenCode and other agents, each in a real terminal with its own interface, so each agent's own config and permission rules apply there unchanged.

The ⌘N New task sheet with its agent picker open: Claude Code and Codex installed, and the other supported agents, including Antigravity CLI, Gemini CLI, GitHub Copilot CLI and OpenCode, marked Not installed.
Every session shows one status: working, needs approval, asked you, done, error or idle. The attention inbox counts the agents blocked on you, and ⌘I (Ctrl+Shift+I on Windows and Linux) jumps to the one that has waited longest. You answer in that agent's own terminal. Hermes never answers a permission prompt for you, so the request waits for a person.

Codex on orders-types and Claude Code on orders-api, side by side, each in its own hermes/ worktree, with status, context use, cost and branch in the sidebar.
Each task can also get its own branch and worktree, so two agents on one repo don't edit the same checkout.
Hermes is free, source-available under BSL 1.1, and runs on macOS, Windows and Linux. It isn't affiliated with Anthropic, OpenAI, Google, GitHub, OpenCode or Nous Research. Download it or read the code on GitHub.
If this was useful
If the human-in-the-loop half is your problem, a star on Hermes IDE's GitHub helps me keep building it.
A headless agent in CI is one more unreliable dependency in your pipeline: it gets rate limited and times out, and its exit code won't always tell you. The System Design Pocket Guide: Fundamentals covers the core building blocks for designing around dependencies like that. It's one of 8 short books in the Pocket Guides for Developers series, covering agents, RAG, prompts, observability, events, databases and system design.
來源:DEV Community · dev.to
