Non-interactive runs and CI automation
codex exec, structured output, sandbox and approval settings for automation, credentials in the environment, and gating a pipeline.
Running without a human
# a single non-interactive task, then exit
codex exec "run the linter and fix only the auto-fixable issues in src/"
# keep the output machine-readable for a later step
codex exec --json "list every TODO comment added in the last 50 commits, as JSON" \
> todos.json
# say exactly what the run may do; do not rely on inherited defaults
codex exec \
--sandbox workspace-write \
--ask-for-approval never \
--cd "$PWD" \
"update CHANGELOG.md with the entries from the last release, then stop"codex execnever prompts. Every permission decision must already be expressed in the sandbox and approval flags, because there is nobody to answer a question.- Output the last message only when a script consumes it; otherwise parse the structured stream. Human-formatted output changes between versions and is a fragile parsing target.
- Always set the working directory explicitly in automation. A run that inherits an unexpected directory writes somewhere you did not intend.
- A non-interactive run should be idempotent where possible: re-running it should converge, not produce a second copy of the same change.
A pipeline job
# .github/workflows/agent-task.yml
name: agent-task
on:
workflow_dispatch:
inputs:
task:
description: "Task for the agent"
required: true
permissions:
contents: read # the default; grant more only if the job must push
jobs:
run-agent:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: "22"
- name: Install dependencies
run: npm ci
- name: Run the agent task
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
CODEX_HOME: ${{ github.workspace }}/.codex-home
run: |
npx --yes @openai/codex exec \
--sandbox workspace-write \
--ask-for-approval never \
"${{ inputs.task }}"
- name: Verify the result
run: |
npm test
npx tsc --noEmit
git diff --stat
- name: Upload the diff for review
if: always()
uses: actions/upload-artifact@v4
with:
name: agent-diff
path: |
**/*.patch
if-no-files-found: ignore
- name: Fail if unexpected files changed
run: |
CHANGED=$(git diff --name-only | grep -v '^src/' || true)
if [ -n "$CHANGED" ]; then
echo "unexpected changes outside src/:"; echo "$CHANGED"; exit 1
fi| Setting | CI choice | Why |
|---|---|---|
| Sandbox | workspace-write | The runner is already disposable |
| Approval | never | Nothing can answer a prompt |
| Credentials | Secret in the environment | Never in a file inside the workspace |
| Timeout | Explicit and short | A looping agent must not hold a runner for an hour |
| Verification | Run the project's own gates | The agent's summary is not evidence |
| Artifact | The diff | A human reviews before anything is merged |
⚠️
A CI runner can reach your secrets and your network. Before enabling any agent job, ask what it can do with the credentials in its environment: a leaked API key, a writable package registry token or a deployment key turns a helpful automation into an exfiltration path. Grant the narrowest token that lets the job finish.
Guardrails that keep a job honest
#!/usr/bin/env bash
# scripts/agent-guard.sh — fail the job when the agent overstepped
set -euo pipefail
ALLOWED_PREFIXES=("src/" "tests/")
MAX_CHANGED_FILES=25
MAX_DIFF_LINES=800
changed=$(git diff --name-only)
count=$(echo "$changed" | grep -c . || true)
if [ "$count" -gt "$MAX_CHANGED_FILES" ]; then
echo "too many files changed: $count"; exit 1
fi
while IFS= read -r file; do
[ -z "$file" ] && continue
ok=false
for prefix in "${ALLOWED_PREFIXES[@]}"; do
case "$file" in "$prefix"*) ok=true ;; esac
done
if [ "$ok" = false ]; then
echo "change outside the allowed paths: $file"; exit 1
fi
done <<< "$changed"
lines=$(git diff --numstat | awk '{a+=$1; d+=$2} END {print a+d+0}')
if [ "$lines" -gt "$MAX_DIFF_LINES" ]; then
echo "diff too large: $lines lines"; exit 1
fi
echo "guard passed: $count files, $lines lines"- Bound the blast radius in code, not in the prompt: a path allowlist and a diff size limit are checkable, whereas "only change what is necessary" is not.
- Never let the job push to main. Produce a branch or a patch artefact and let a human merge it.
- Make the job re-runnable. If a second run produces a conflicting change, the job is not safe to trigger twice.
- Log the task text, the model, the version and the resulting diff hash together. Reproducing an unexplained CI change months later depends on that record.
FAQ
Should an agent job have write access to the repository?
Only to push a branch, and only if the branch cannot be merged without a human approval. Granting write access to main converts every agent mistake into an incident.
How do I keep CI costs predictable?
Set a short timeout, use the lowest reasoning effort that works, restrict the job to a labelled trigger rather than every pull request, and cap the changed-line count so a runaway run fails fast.
Related
Approval modes and sandboxing Security, cost and team practices
Last refreshed 2026-09-18.