# Loops in Codex

Codex loops with Goal mode (/goal) for work-check-continue, a Stop hook for harness-enforced continuation, and scheduled tasks or codex exec in CI for recurring runs.

Source: https://ai-sw-factory.mellicci.dev/fundamentals/loops/codex

## At a glance

| | Codex |
|---|---|
| Term | Goal mode (`/goal`) for goal loops; scheduled tasks for recurring runs; `Stop` hook and `codex exec` as building blocks |
| Configured in | `/goal` in the desktop app, CLI or IDE extension; **Scheduled** view in the desktop app or web; `.codex/hooks.json`; a CI workflow |
| Loads / runs | Goal mode continues the active chat until Codex judges the goal done; a scheduled task starts at its time; `Stop` fires when a turn ends |
| Scope & precedence | A goal belongs to one chat. Goals and scheduled tasks use your sandbox and approval settings; they don't widen them |

## Build the scenario

| Step | What you write or run | Where |
|---|---|---|
| 1 | Start the flaky-test goal with `/goal` | CLI, IDE or desktop app chat |
| 2 | Optional: a `Stop` hook that re-runs `pnpm test` and continues until 3 passes | `.codex/hooks.json`, `.codex/hooks/loop-check.sh` |
| 3 | Nightly triage as a scheduled task, or a scheduled CI job running `codex exec` | Desktop app or web **Scheduled**; `.github/workflows/` |

**Goal loop.** The goal text is both the first prompt and the completion criteria, so it carries the check, the cap and the notes location:

```text
/goal Make the ticket-service test suite green. Done when `pnpm test`
passes 3 runs in a row. Stop after 10 rounds, or if the same test fails
the same way twice. After each round append what you tried to
.agent/loop-notes.md and commit. Don't skip or delete tests.
```

The documentation doesn't describe a round limit for goals, so "10 rounds" here is an instruction the model follows, not a harness setting. For a limit the harness enforces, add a `Stop` hook:

```json
{ "hooks": { "Stop": [{ "hooks": [{ "type": "command", "timeout": 300,
  "command": "\"$(git rev-parse --show-toplevel)/.codex/hooks/loop-check.sh\"" }] }] } }
```

```bash
#!/usr/bin/env bash
cd "$(git rev-parse --show-toplevel)" && mkdir -p .agent
s=.agent/loop-state; read -r rounds passes < "$s" 2>/dev/null || { rounds=0; passes=0; }
rounds=$((rounds+1))
if pnpm test >/dev/null 2>&1; then passes=$((passes+1)); else passes=0; fi
echo "$rounds $passes" > "$s"
[ "$passes" -ge 3 ] || [ "$rounds" -ge 10 ] && exit 0
echo "{\"decision\":\"block\",\"reason\":\"Round $rounds: $passes/3 green runs. Fix failures, log to .agent/loop-notes.md, commit.\"}"
```

Delete `.agent/loop-state` before each new loop. The documentation doesn't say how a `Stop` hook and Goal mode interact; try them separately first.

**Nightly triage.** In the desktop app, ask Codex to create a standalone scheduled task with this saved prompt, nightly, in a worktree:

```text
List tests that failed in CI since yesterday (`gh run list`, `gh run view --log-failed`).
Find the open issue labeled `flaky`; if none, create one. Update its body:
one row per test with first seen, last seen, fail count. If nothing new
failed, change nothing and report that. Never edit source files.
```

The state lives in the issue, so each run, which starts a new chat, reads it first. To run outside the desktop app, use a scheduled CI job:

```yaml
on:
  schedule: [{ cron: "0 3 * * *" }]
jobs:
  triage:
    runs-on: ubuntu-latest
    permissions: { contents: read, issues: write }
    steps:
      - uses: actions/checkout@v5
      - uses: openai/codex-action@v1
        with:
          openai-api-key: ${{ secrets.OPENAI_API_KEY }}
          prompt: Triage failing tests and update the issue labeled `flaky`.
```

The documentation doesn't specify how the action's Codex run authenticates `gh` to write issues; check the action's security guidance before granting `issues: write`.

## Specifics

### Goal loops

| Feature | What it is | Start | Stops at | Runs where |
|---|---|---|---|---|
| Goal mode | Persistent goal attached to the active chat; Codex keeps working | `/goal <objective>` | Codex judges it done; you pause or clear; it pauses for a decision | Desktop app, CLI, IDE extension |
| Goal controls | View and manage | `/goal`, `/goal edit`, `pause`, `resume`, `clear` | n/a | Same; the app adds a progress row |
| Stop hook loop | Harness script that blocks the end of a turn | `Stop` hook in `hooks.json` | Your script exits 0 without `block` | Wherever hooks run |
| `codex exec` | One non-interactive run | `codex exec "<prompt>"` | The run ends; not a loop by itself | CLI, CI |

The objective must be non-empty and at most 4,000 characters; for more, point the goal at a file. The documentation recommends outcome, constraints and verification in the goal, and `/plan` first if the outcome is unclear.

### Stop conditions and limits

| Condition | Documented? | How |
|---|---|---|
| Success | Yes | Codex decides the goal is met; a `Stop` hook can make the check deterministic |
| Iteration cap | No setting for goals | Put it in the goal text, or count rounds in a `Stop` hook |
| Budget or time cap | No setting for goals | The documentation doesn't specify a configurable cap; put it in the goal text or a `Stop` hook |
| Human stop | Yes | `/goal pause` or `/goal clear`; interrupt the turn |
| Needs a decision | Yes | The goal pauses under the same sandbox and approval policy |
| `Stop` hook default | Yes | `timeout` 600 s unless set; `continue: false` from any matching `Stop` hook wins |

### Progress between iterations

| Mechanism | Persists | Note |
|---|---|---|
| Same chat | Context and goal | Goal mode continues in the chat; each chat has its own goal |
| `.agent/loop-notes.md` and commits | Files and Git history | You ask for them in the goal text |
| Issue labeled `flaky` | GitHub | Standalone scheduled tasks start a new chat each run |
| Task inside a chat | Chat context | Returns to the same chat each run |
| `codex exec resume --last` | Session | Continues a previous non-interactive session |

### Recurring and scheduled runs

| Option | Start | Cadence | Runs where |
|---|---|---|---|
| Standalone scheduled task | Ask in a chat, or **Scheduled** | Custom schedule controls, RRULE | Desktop app (local project or worktree); web, without local folders |
| Task inside a chat | Ask Codex to schedule it in the chat | Minutes, daily, weekly | Returns to that chat and its context |
| Event-triggered task | Ask in web or mobile | Gmail, Slack, GitHub PR events; can't combine with a time schedule | Web and mobile only, eligible plans |
| `codex exec` in CI | Your scheduler | Yours | CI runner |

Scheduled management isn't available in the CLI or IDE extension. Local tasks need the computer on and the desktop app running. Scheduled tasks run unattended with your sandbox settings and use `approval_policy = "never"` when policy allows.

### Hooks as loop control

| Item | Detail |
|---|---|
| Event | `Stop`, when a turn ends; `matcher` is ignored |
| Input | `stop_hook_active`, `last_assistant_message`, `turn_id`, plus common fields |
| Continue | stdout JSON `{"decision":"block","reason":"..."}` or exit 2 with the reason on stderr; `reason` becomes a new user prompt |
| Finish | Exit 0 with JSON output or none; `continue: false` ends the hook run and takes precedence |
| Output rule | On exit 0, `Stop` expects JSON on stdout; plain text is invalid |

## Gotchas

- **The cap is yours to build.** Goals document no iteration or budget setting; put caps in the goal text and, for a hard stop, in a `Stop` hook.
- **Hook output format.** A `Stop` hook that prints plain text on exit 0 is invalid; print JSON or nothing.
- **Desktop-only scheduling.** A local scheduled task needs the computer on and the app running; the CLI only prepares and tests the prompt.
- **Unattended means sandboxed.** Start a scheduled task with the narrowest access, and use a worktree so it doesn't touch files you are editing.
- **Two chats, one checkout.** Each chat has its own goal; don't let two chats change the same files.
