codex exec Headless Modes: Unattended Is a Different Trust Tier
codex exec headless runs, Claude Code print mode, and Actions wrappers need their own trust tier: flag shapes per tier, no prod creds, an evidence pack per run.
Go deeper. Build your own.
Nobody is watching the run that matters most. The interactive session gets your eyes on every diff. The scheduled one at 03:00 gets a runner, a token, and whatever a stranger typed into the issue that triggered it. Same harness, same model, the same flags if you copied the wrapper from your laptop, and a completely different set of people who can put text in front of it.
A codex exec headless run is the cleanest example, because the docs say it plainly. Non-interactive mode “lets you run Codex from scripts (for example, continuous integration (CI) jobs) without opening the interactive TUI,” and one of its listed uses is to “run with explicit, pre-set sandbox and approval settings” (Codex non-interactive mode). Explicit and pre-set are the operative words. With no dialog to fall back on, every trust decision gets made before the prompt, by you, in a flag line someone can diff.
This playbook gives you three tiers (interactive, supervised batch, unattended), a per-tier table with concrete flag shapes for Codex and Claude Code, the GitHub Actions job split that keeps the model key away from the write token, and the evidence pack an unattended run must leave behind. Black Hat 2026 supplied the reason to do it this month. The tiers outlast the advisory.
What the docs admit and what Black Hat added, once
The Codex non-interactive page is a trust-tier document that does not call itself one. Its permissions section makes --sandbox read-only the default for codex exec, with workspace-write to allow edits and danger-full-access for controlled environments; --full-auto now prints a deprecation warning. It says to pass CODEX_API_KEY inline for one invocation and never as a job-level variable in a workflow that checks out untrusted code. It requires a Git repository “to prevent destructive changes,” with --skip-git-repo-check as the override, and its automation example splits a read-only Codex job from a follow-up PR job that applies patches without receiving the OpenAI credential (Codex non-interactive mode; Codex CLI).
Screenshot: OpenAI Developers, “Non-interactive mode,” captured Sep 13, 2026.
Claude Code’s permissions page makes the same admission from the other side. A claude -p or SDK session never shows the trust dialog, and in a folder you never trusted it still uses the repository’s hooks, env block, and helper commands, and connects .mcp.json servers “without asking, approved or not” (Claude Code permissions). The mode with no human present runs more of the repository’s code before the prompt, not less.
Black Hat 2026 (Aug 5–6, 2026) showed what happens when that gap meets a stranger’s input. Novee Security’s post, dated Aug 6, 2026, walked through a Codex two-pass triage workflow in which pass 1, steered by an issue body, wrote AGENTS.md for pass 2 to obey, and a Gemini CLI headless run whose fix is described as “a breaking change to how non-interactive headless environments handle folder trust” (Novee Security). The full account, with compensating controls, is in the sandbox is a suggestion. The narrower point here: the defaults were fine for a person at a keyboard and wrong for a runner, and no tier existed to say so.
Why a codex exec headless run is a tier, not a flag
Four things change when the human leaves the loop, and none of them is the model.
The approval path disappears. Interactive Codex asks; interactive Claude Code asks. Headless, a prompt either hangs the job until its timeout or gets auto-denied. Whatever answer you would have given has to be encoded before the run, which is what --ask-for-approval never and Claude Code’s dontAsk mode exist for.
Input provenance flips. At the keyboard the prompt is yours. In an issues: opened workflow the prompt belongs to whoever has a GitHub account, and Novee counted 114 public repositories running a pattern any issue could trigger.
Repository config runs first. The -p trust table is the Claude Code version; AGENTS.md and .rules are the Codex version; .git/config is everyone’s. Interactive sessions at least show a dialog. Headless sessions show a log line, later.
Nobody notices for hours. Blast radius is token scope multiplied by the time until someone reads the output. A coordinator that runs on a schedule and fixes CI on its own, which Cursor’s Aug 19, 2026 changelog offers for cloud agents (Cursor changelog), is unattended by definition. Agentic CI/CD covers the pipeline shape; this piece covers what the pipeline may hold.
The three-tier model: interactive, supervised batch, unattended
Interactive means a human at the keyboard, input the human wrote, approvals answered live. Supervised batch means the run itself is unattended but a human reviews its output before anything applies, and the input comes from the team or known contributors. Unattended means no human before, during, or immediately after; triggers are schedules, webhooks, or public issues, and the run may only propose.
As the human leaves the loop, the surface the run may touch narrows. Illustrative operator model.
| Control | Interactive | Supervised batch | Unattended |
|---|---|---|---|
| Who is watching | you, live | you, before apply | nobody; a schedule, a webhook, a stranger |
| Codex sandbox | --sandbox workspace-write |
--sandbox workspace-write in a throwaway checkout |
--sandbox read-only (the codex exec default) |
| Codex approvals | --ask-for-approval on-request |
--ask-for-approval never, output reviewed |
--ask-for-approval never |
| Claude Code mode | default or acceptEdits |
-p --permission-mode plan, or dontAsk with allow rules |
-p --permission-mode dontAsk --bare --setting-sources user |
| Codex repo config | trusted after the dialog | --ignore-rules |
--ignore-rules --ignore-user-config; instruction files hash-pinned |
| Claude repo config | trusted after the dialog | --setting-sources user |
--bare --setting-sources user; hooks off |
| Credentials | your scoped login | agent identity; key inline per invocation | agent identity; key inline; contents: read; no production credentials |
| Writes | your working tree | throwaway checkout; patch as artifact | patch as artifact; a separate gated job applies it |
| Evidence | transcript | --json stream, last message, diff |
full pack, exported before the runner dies |
Step 1: Classify every run by who is watching and who wrote the input
Two questions decide the tier, and the second overrides the first. Is a human reviewing output before anything applies? Can someone outside the org put text into the run? If the second answer is yes, the run is unattended regardless of how attentive the human plans to be, because the human is reviewing what the attacker wanted them to see.
Write the inventory down as four columns: run, trigger, who can write the input, tier. codex in your terminal is interactive. A nightly dependency triage on cron, fed by maintainers through the repo, is supervised batch. An issue labeler on issues: opened, a PR fixer that accepts forks, and a scheduled coordinator watching a chat channel are all unattended, because in each case the input author is anyone with an account. The third column is the one people get wrong.
Step 2: Pin the tier’s flag line in the wrapper, and diff it weekly
A tier is a flag line, checked into the repo, owned by CODEOWNERS. Two shapes for the unattended tier:
# Unattended Codex, using flags documented on Sep 13, 2026.
CODEX_API_KEY="$KEY_FOR_THIS_STEP_ONLY" \
codex exec --sandbox read-only --ask-for-approval never \
--ignore-rules --ignore-user-config \
--json --output-last-message evidence/last.md \
--output-schema ./triage.schema.json \
"Read evidence/issue.json. Propose a fix as a unified diff. Do not run install scripts."
# Unattended Claude Code, using current documented modes and output flags.
claude -p "Read evidence/issue.json and propose a fix as a unified diff" \
--permission-mode dontAsk --bare --setting-sources user \
--settings '{"disableAllHooks": true}' \
--disallowedTools "Bash(curl *)" "Bash(wget *)" "WebFetch" \
--output-format json --max-turns 20
Screenshot: OpenAI Developers, Codex CLI docs, captured Sep 13, 2026.
Two things to grep out of every wrapper: --skip-git-repo-check, which removes the “prevent destructive changes” floor, and --full-auto, which the docs now deprecate in favor of an explicit --sandbox workspace-write. Either one in an unattended wrapper is a tier violation, not a style choice. Note also that --bare leaves the project’s env block in force, per the docs, which is one more reason the key lives in a single step and nowhere else.
Step 3: Strip the environment: no production credentials, key inline, one identity per job
The Codex docs’ own automation pattern is the shape: a read-only agent job that holds the model key for one step, and an apply job that holds contents: write and never sees the model key (GitHub Docs).
# .github/workflows/agent-triage.yml (illustrative shape, not a vendor template)
on: { issues: { types: [opened] } }
permissions: { contents: read } # the workflow-wide floor
jobs:
propose:
runs-on: ubuntu-latest
permissions: { contents: read, issues: read }
steps:
- uses: actions/checkout@v4
- run: mkdir -p evidence && gh issue view "$NUM" --json author,authorAssociation,body > evidence/issue.json
env: { GH_TOKEN: '${{ github.token }}', NUM: '${{ github.event.issue.number }}' }
- run: | # key scoped to this one step, never job-level env
CODEX_API_KEY="${{ secrets.CODEX_API_KEY }}" codex exec --sandbox read-only \
--ask-for-approval never --json -o evidence/last.md "Read evidence/issue.json ..."
- uses: actions/upload-artifact@v4
with: { name: evidence, path: evidence }
apply:
needs: propose
runs-on: ubuntu-latest
environment: agent-writes # required reviewers live here
permissions: { contents: write, pull-requests: write }
steps: [] # applies the patch, opens the PR; no model key
Production credentials do not appear anywhere in this file, at any tier. Cloud access, if the apply job needs it, comes from OIDC federation in that job only, minted per run and expiring in minutes; the agent job gets none. The identity side of that, one principal per agent and no human SSO in the loop, is agent as service principal.
Step 4: Allowlist by tier: tools, domains, paths
Unattended runs get the shortest lists you can write, and the lists live in files that reviewers own. Network at that tier is the model endpoint and a package mirror, nothing else. For Codex, a required MCP server that fails to initialize makes codex exec exit with an error, which is the fail-closed behavior you want. For Claude Code, deny rules such as Bash(curl *) and Bash(wget *), a WebFetch(domain:...) allowlist for the one or two hosts the task needs, and permissions.blockReadsOutsideWorkingDirectories fence the run; managed settings can set permissions.disableBypassPermissionsMode to "disable" so nobody’s wrapper can escalate (Claude Code permissions).
The docs add a caution worth copying into your policy: a Bash deny rule does not match the same program by path or inside sh -c, so pair it with the sandbox’s network allowlist when the restriction must hold. Deny rules are the label; the sandbox allowlist is the wall. Keeping one dialect for those rules across Codex, Claude Code, and whatever coordinator sits above them is its own playbook, and a restricted-mode fleet policy is where the unattended row becomes a fleet default rather than a per-repo choice.
Step 5: Approval without a human: deterministic gates, then the write job
At the unattended tier, “approve” is a predicate, not a person. The proposal passes to the apply job only if every gate is true:
- The instruction files (
AGENTS.md,CLAUDE.md,.rules) hash the same at the end of the run as at the start. - The diff touches no path under
.github/workflows/, no lockfile, and no file matching your secrets patterns. - The diff is under a size cap you chose in advance (a few hundred lines is a common ceiling).
- The
--jsonstream contains zero denied tool calls. - The
--output-schemavalidation passed; an invalid answer is a failed run, never a retry with looser settings.
Only then does a human see it, in the apply job’s environment, with the evidence pack attached. That human is approving a patch with provenance, not a chat transcript, and the approval is a click on a job that holds no model key. I have never regretted making a gate a predicate; I have regretted every gate that was a person’s good mood.
Step 6: The evidence pack every unattended run leaves behind
Illustrative. “Enforced” means managed settings, branch protection, or a required-reviewer environment make the control impossible to skip.
Unattended runs must be reconstructible without the vendor, because the runner is gone within minutes and the vendor’s transcript view rotates on its own schedule. If the pack is missing, the postmortem becomes an argument about what the model probably did. The pack, per run: events.jsonl from --json; last.md from --output-last-message; the structured output that passed --output-schema; patch.diff derived from that validated output; issue.json with the author and their repository association; env-names.txt (names only, never values); instructions.sha256 before and after; the wrapper’s flag line as executed; and the session id so codex exec resume can pick the thread up under supervision. Upload it as an artifact, then copy it to a store you control. Fleet replay is the practice that turns that folder into an answer six weeks later.
Failure signals for headless runs: hangs, silent denials, and files that came back changed
- The job runs to its timeout with no final message. An approval prompt was waiting for nobody. Pin
--ask-for-approval neverordontAskand rerun; do not raise the timeout. - The run “succeeded” with denied tool calls in the stream. Read the denied calls before the diff; in the unattended tier, more than zero is a page.
- An instruction file’s hash changed mid-run. That is the two-pass pattern. Quarantine the branch and keep the artifact; it is the evidence.
codex execexited because an MCP server failed to initialize. Good. Fix the server or remove it from the tier’s list; never adddanger-full-accessto make the error go away.- The same wrapper has different flag lines across repos. Run a weekly diff of every
codex execandclaude -pinvocation in.github/; drift is the leading indicator of a tier violation.
Tiers are policy; the fleet needs somewhere for policy to live
A tier is only useful if the fleet can tell which tier a run was in after the fact. That means the flag line, the identity, the input’s provenance, and the evidence pack all land somewhere an operator can search, whether the run happened on a runner, a laptop, or a vendor’s managed harness. The Codex deep dive covers what the harness itself does; the tier model is what the desk wraps around it.
That is the operating-layer argument once more: agents act, so the desk needs an operating layer rather than a smarter prompt. A multi-agent command center is where “which tier did run 4182 execute in, and who wrote its input” becomes a lookup. The vendor gives you --sandbox read-only. Deciding when it is mandatory is yours.
FAQ: codex exec headless runs
Is codex exec safe to run in GitHub Actions on a public repository?
Only at the unattended tier: --sandbox read-only, --ask-for-approval never, the API key inline for one step, repository rules ignored, a patch as the only output, and a separate gated job holding contents: write. The wrappers Black Hat 2026 examined were not that.
What is the default sandbox for codex exec?
Per the Codex docs, codex exec defaults to --sandbox read-only. workspace-write allows edits inside the workspace, danger-full-access removes the sandbox for controlled environments, and --full-auto is deprecated in favor of an explicit --sandbox workspace-write. Pin the flag in the wrapper anyway, so the tier is visible in a diff.
Should a headless agent ever hold production credentials?
No. The agent job holds a read-only repository token and a model key scoped to one step. Anything that touches production runs in a separate job with its own identity, minted per run through OIDC, behind required reviewers. If the agent can reach production, the tier model has already failed.
