
Codex Review
- 29 installs
- 177 repo stars
- Updated May 10, 2026
- artwist-polyakov/polyakov-claude-skills
codex-review is a Claude skill that runs a cross-agent review workflow where Codex reviews Claude's plan and code and drives iteration through exit-code verdicts.
About
This skill runs a cross-agent review workflow in which Claude implements and Codex (GPT) reviews both the plan and the resulting code in the same directory. A developer uses it to get a second, independent technical review before shipping, iterating until Codex approves or escalates. It manages a Codex session, passes plan and code descriptions to Codex, and interprets exit codes to decide whether to proceed, revise, or escalate. Documentation is primarily in Russian.
- Cross-agent review workflow where Claude implements and Codex reviews
- Reviews both the plan (before ExitPlanMode) and the implemented code
- Uses exit codes (APPROVED, CHANGES_REQUESTED, ESCALATE, NO_SESSION) to drive iteration
Codex Review by the numbers
- 29 all-time installs (skills.sh)
- Ranked #676 of 1,352 Code Review & Quality skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
codex-review capabilities & compatibility
- Capabilities
- agent deck
- Use cases
- code review
- Pricing
- Free
What codex-review says it does
Кросс-агентное ревью: Claude реализует, Codex (GPT) ревьюит.
Все вызовы codex-review.sh и codex-state.sh ОБЯЗАНЫ выполняться с `dangerouslyDisableSandbox: true`
npx skills add https://github.com/artwist-polyakov/polyakov-claude-skills --skill codex-reviewAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 29 |
|---|---|
| repo stars | ★ 177 |
| Last updated | May 10, 2026 |
| Repository | artwist-polyakov/polyakov-claude-skills ↗ |
What it does
Run a cross-agent review where Codex reviews Claude's plan and code before shipping, iterating on exit-code verdicts.
Who is it for?
Developers who want an independent Codex/GPT review of a plan and code before shipping
When should I use this skill?
The user says 'codex review', 'with codex review', or 'start codex review'
What you get
A plan and implementation that have passed an independent Codex technical review, or a clear escalation.
- plan review verdict
- code review verdict
- review notes
By the numbers
- 4 exit-code statuses (APPROVED, CHANGES_REQUESTED, ESCALATE, NO_SESSION)
Files
Codex Review Workflow
Кросс-агентное ревью: Claude реализует, Codex (GPT) ревьюит. Codex работает в той же директории и может самостоятельно смотреть код.
Расположение скриптов
Скрипты лежат в scripts/ рядом с этим SKILL.md. Определи полный путь:
- Этот файл: путь из которого ты прочитал SKILL.md
- Скрипты: замени
SKILL.mdнаscripts/codex-review.sh(иscripts/codex-state.sh)
Все команды ниже используют относительный scripts/ — подставь полный путь при вызове.
CRITICAL: Sandbox
Codex CLI использует macOS system API (SCDynamicStore), которые блокируются sandbox Claude Code. Все вызовы codex-review.sh и codex-state.sh ОБЯЗАНЫ выполняться с `dangerouslyDisableSandbox: true` в Bash tool. Без этого codex крашится с паникой Rust.
Workflow
1. Инициализация сессии
Создай сессию Codex с описанием задачи.
bash scripts/codex-review.sh init "Implement JWT authentication for API"Сессия может быть также задана вручную в .codex-review/config.env: CODEX_SESSION_ID=sess_...
Если сессии нет (exit 3 — NO_SESSION), спроси пользователя:
- Есть ли уже живая сессия с Codex? → пусть впишет id в config.env
- Или создать новую через
init?
2. Ревью плана
Передай путь к файлу плана через --plan-file. НЕ вставляй содержимое плана в аргумент командной строки — скрипт сам читает файл и передаёт содержимое inline в Codex.
С plan mode
Если используешь plan mode — отправь план на ревью перед ExitPlanMode: 1. Написал план → CC сохраняет его в ~/.claude/plans/<slug>.md (автоматически) 2. Передай этот путь в --plan-file:
bash scripts/codex-review.sh plan --plan-file ~/.claude/plans/<slug>.md3. CHANGES_REQUESTED → скорректируй план в файле, отправь снова (см. «Accept or Argue») 4. APPROVED → вызови ExitPlanMode для одобрения пользователем
Таким образом план проходит два ревью: техническое (Codex) и бизнес-приоритетное (пользователь).
Без plan mode
Если план написан в отдельный файл внутри проекта:
bash scripts/codex-review.sh plan --plan-file docs/plan.mdШаблон плана (рекомендуемая структура файла)
What: [problem being solved]
Approach: [chosen approach and why]
Alternatives considered: [what was rejected and why]
Files to change: [list]
Addressed concerns: [if resubmit — point-by-point from previous review]3. Реализация
Перед началом реализации обнови фазу:
bash scripts/codex-state.sh set phase implementingИмплементируй по утвержденному плану.
4. Ревью кода
Опиши ЧТО сделал, КАКИЕ решения принимал. НЕ передавай git diff — Codex сам посмотрит.
Шаблон описания кода
What changed: [summary of changes]
Key decisions: [non-obvious decisions made during implementation]
Files modified: [list with brief description per file]
Tests: [what tests were added/run, results]
Addressed concerns: [if resubmit — point-by-point from previous review]bash scripts/codex-review.sh code "What changed: JWT auth middleware + refresh endpoint. Key decisions: RS256 over HS256 for key rotation. Files: auth/jwt.py (middleware), api/auth.py (refresh endpoint). Tests: 3 new tests (expired/invalid/valid tokens), all pass."5. Управление состоянием
bash scripts/codex-state.sh show # Текущее состояние
bash scripts/codex-state.sh dir # Путь к state-каталогу текущей ветки
bash scripts/codex-state.sh reset # Сброс итераций (session сохраняется)
bash scripts/codex-state.sh reset --full # Полный сброс
bash scripts/codex-state.sh get session_id # Получить поле
bash scripts/codex-state.sh set session_id <val> # Установить вручную
bash scripts/codex-state.sh set phase implementing # Обновить фазуДля чтения файлов ревью (notes, STATUS.md и пр.) используй codex-state.sh dir — он вернёт абсолютный путь к каталогу текущей ветки.
Обработка exit-кодов
| Exit | Status | Действие |
|---|---|---|
| 0 | APPROVED | Продолжай работу |
| 0 | CHANGES_REQUESTED | Скорректируй и отправь снова (см. «Accept or Argue») |
| 1 | ERROR | Сообщи об ошибке, предложи проверить session_id |
| 2 | ESCALATE | Оповести пользователя, выведи краткое резюме, предложи варианты (см. «Обработка ESCALATE») |
| 3 | NO_SESSION | Спроси: создать сессию через init? |
Обработка ESCALATE (exit 2)
Когда лимит итераций исчерпан:
1. Получи путь: STATE_DIR=$(bash scripts/codex-state.sh dir). Прочитай заметки ревью из $STATE_DIR/notes/ (файлы {phase}-review-{N}.md) 2. Выведи пользователю краткое резюме:
- Какой этап (plan/code), сколько итераций прошло
- Ключевые замечания и статусы по каждой итерации (1-2 строки на итерацию)
3. Используй AskUserQuestion с тремя вариантами:
- Ещё одна итерация — разово расширить лимит на 1
- Снять лимит — убрать ограничение для этой сессии
- Прекратить ревью — вывести финальное резюме и остановиться
(Вариант «Свой вариант» добавляется автоматически)
Обработка ответа:
- «Ещё одна итерация» → повтори вызов
codex-review.sh {phase} "..." --max-iter $((текущий_лимит + 1)) - «Снять лимит» → повтори вызов
codex-review.sh {phase} "..." --max-iter 999 - «Прекратить ревью» → выведи финальное резюме и заверши процесс ревью
- Свой вариант → следуй инструкции пользователя
STATUS.md
Файл STATUS.md в state-каталоге ветки (путь: codex-state.sh dir) создаётся и обновляется автоматически скриптами. Не редактируй его вручную.
- Файл появляется при
initи обновляется при каждомplan/codeиcodex-state.sh set - Файл удаляется при финальном APPROVED на этапе
codeи приreset --full - Наличие файла = активное ревью, отсутствие = ревью не идёт
Verdict
Codex пишет свой вердикт в verdict.txt внутри state-каталога ветки (одно слово: APPROVED или CHANGES_REQUESTED). Для чтения вердикта используй `bash scripts/codex-state.sh get verdict` — helper возвращает APPROVED, CHANGES_REQUESTED или пустую строку (нет/невалидно). Файл очищается перед каждым запросом ревью. Если Codex не создал файл — скрипт парсит вердикт из текста ответа (fallback). Плагинный хук ExitPlanMode дополнительно связывает вердикт с текущей Claude-сессией через current_session.txt в том же каталоге — verdict, пришедший из другой сессии, удаляется.
Правила
- НИКОГДА не вызывай
codex execнапрямую — только через скриптыcodex-review.shиcodex-state.sh. Скрипты сами знают модель, конфиг и session_id - Описывай ЧТО ты сделал и ПОЧЕМУ, какие решения принимал — используй шаблоны описания
- НЕ передавай git diff — Codex сам посмотрит, он в той же директории
- APPROVED → продолжай работу
- Перед реализацией вызови
codex-state.sh set phase implementing - Есть заказчик (пользователь) — уточняй у него неоднозначные вопросы
- Опция
--max-iter Nпозволяет изменить лимит итераций
Worktree & Branch Isolation
Состояние ревью изолировано по ветке. Скрипты автоматически определяют основной репозиторий и текущую ветку. Параллельная работа на нескольких ветках/worktrees безопасна. config.env — общий (в корне .codex-review/). Для получения пути к state-каталогу текущей ветки используй codex-state.sh dir.
Auto-Workflow (AUTO_REVIEW=true)
When AUTO_REVIEW=true in .codex-review/config.env, the entire review cycle runs automatically. A plugin hook blocks ExitPlanMode until Codex approves the plan.
Plan phase
1. Write the plan in plan mode as usual 2. Before calling ExitPlanMode, run review:
bash scripts/codex-review.sh init "task description" # ALWAYS init for a new plan — archives previous session
bash scripts/codex-review.sh plan --plan-file ~/.claude/plans/<slug>.mdIMPORTANT: Always run init before the first plan review in a conversation. Even if codex-state.sh show reports an existing session, it may be stale (from a previous conversation). The init command safely archives the old session and creates a fresh one. Only skip init when re-submitting after CHANGES_REQUESTED within the same review cycle. 3. Formal verdict check — run bash scripts/codex-state.sh get verdict. Proceed ONLY if it outputs the exact string APPROVED. Do NOT interpret review text — only the helper output matters. 4. CHANGES_REQUESTED → fix the plan, resubmit (follow «Accept or Argue» rules). Iterate automatically up to the iteration limit. 5. APPROVED → call ExitPlanMode (the hook auto-approves it)
Implementation phase
6. Implement as usual. Set phase: bash scripts/codex-state.sh set phase implementing
Code phase
7. After implementation, run code review:
bash scripts/codex-review.sh code "code description"8. Formal verdict check — same as step 3: run bash scripts/codex-state.sh get verdict and check for exact string APPROVED. 9. CHANGES_REQUESTED → fix code, resubmit automatically. 10. APPROVED → work is complete, report to user.
ESCALATE handling in auto mode
Same as standard ESCALATE handling — present summary and ask user via AskUserQuestion.
Accept or Argue
При получении CHANGES_REQUESTED:
1. Прочитай предыдущую review note из $(bash scripts/codex-state.sh dir)/notes/{phase}-review-{N}.md 2. Критически оцени каждое замечание. В описании к повторной отправке ОБЯЗАТЕЛЬНО адресуй каждое замечание поточечно:
- Исправлено: [что именно исправил и как]
- Не согласен: [контраргумент с обоснованием — Codex видит историю и может принять или настоять]
- Отложено: [причина — только с согласия пользователя через AskUserQuestion]
3. Если одно и то же замечание повторяется 2+ раза без нового содержания (Codex настаивает, ты уже аргументировал) — эскалируй пользователю через AskUserQuestion: покажи замечание, свои аргументы, и спроси решение 4. При исчерпании лимита итераций — следуй процедуре «Обработка ESCALATE»
config/.env
# Existing Codex session (optional — or use `init` to create one)
# CODEX_SESSION_ID=sess_your_session_id
CODEX_MODEL=gpt-5.2
CODEX_REASONING_EFFORT=high
CODEX_MAX_ITERATIONS=5
CODEX_YOLO=true
# Auto-review mode: when enabled, ExitPlanMode is blocked until Codex
# approves the plan, and code review runs automatically after implementation.
# AUTO_REVIEW=true
# Custom init procedure (optional, controls what Codex does during init)
# Reviewer role is always set automatically — this only adds init instructions.
# CODEX_REVIEWER_PROMPT="Explore the codebase areas relevant to the task. Understand the architecture, patterns, and conventions so you are prepared to review."
# Phase-specific review guidance (optional, appends to built-in focus areas)
# CODEX_PLAN_GUIDE=""
# CODEX_CODE_GUIDE=""
Codex Review Plugin
Кросс-агентное ревью: Claude Code реализует, Codex (GPT) ревьюит.
ВАЖНО! Скрипты плагина хранят всё состояние (сессию, конфиг, журнал ревью) в директории.codex-review/в корне вашего проекта, а не рядом с собой. Директорияconfig/внутри плагина — это только шаблон. Не редактируйте файлы в директории установки плагина (~/.claude/plugins/...) — они перезапишутся при обновлении.
Установка
Вариант A: через marketplace (рекомендуется)
1. Добавь репозиторий как marketplace (один раз):
# Из локальной директории
claude plugin marketplace add /path/to/polyakov-claude-skills
# Или из GitHub
claude plugin marketplace add github:artwist-polyakov/polyakov-claude-skills2. Установи плагин:
claude plugin install codex-review@polyakov-claude-skillsВариант B: для одной сессии
claude --plugin-dir /path/to/polyakov-claude-skills/plugins/codex-reviewЗависимости
Убедись, что codex CLI установлен:
npm install -g @openai/codexНастройка проекта
.gitignore
Добавь в .gitignore (или .git/info/exclude) проекта:
.codex-review/config.env
.codex-review/*/state.json
.codex-review/*/STATUS.md
.codex-review/*/verdict.txt
.codex-review/*/last_response.txt
.codex-review/*/codex-*.log
.codex-review/archive/notes/ НЕ игнорируем — это журнал ревью для команды.AGENTS.md (для Codex)
Добавь в AGENTS.md проекта секцию:
## Review Protocol
Если ты выступаешь ревьювером (запущен через codex-review workflow):
- Давай конкретный actionable фидбек
- Можешь смотреть код/diff самостоятельно
- Не запускай скрипты из skills/codex-review/ — ты ревьюер
- Не заглядывай в .codex-review/archive/ — там артефакты прошлых сессий
- После ревью запиши вердикт по пути, указанному в промпте ревью (одно слово: APPROVED или CHANGES_REQUESTED)settings.local.json
Добавь разрешения в .claude/settings.local.json:
{
"permissions": {
"allow": [
"Bash(bash */codex-review.sh:*)",
"Bash(bash */codex-state.sh:*)",
"Bash(codex exec:*)"
]
}
}Конфигурация (опционально)
Создай .codex-review/config.env в корне проекта:
# Существующая сессия Codex (или используй init для создания новой)
# CODEX_SESSION_ID=sess_your_session_id
CODEX_MODEL=gpt-5.2
CODEX_REASONING_EFFORT=high
CODEX_MAX_ITERATIONS=5
CODEX_YOLO=true
# Auto-review mode: block ExitPlanMode until Codex approves the plan,
# auto-run code review after implementation.
# AUTO_REVIEW=true
# Custom init procedure (optional, controls what Codex does during init)
# Reviewer role is always set automatically — this only adds init instructions.
# Example: make Codex explore the codebase before reviews begin:
# CODEX_REVIEWER_PROMPT="Explore the codebase areas relevant to the task. Understand the architecture, patterns, and conventions so you are prepared to review."
# Additional guidance for plan review phase (optional, appended to built-in focus areas)
# CODEX_PLAN_GUIDE="Verify backward compatibility with API v1 clients"
# Additional guidance for code review phase (optional, appended to built-in focus areas)
# CODEX_CODE_GUIDE="Check that all DB queries use parameterized statements"Использование
Подключение существующей сессии Codex
Если у вас уже есть живая сессия с Codex (например, вы обсуждали архитектуру), впишите её id в .codex-review/config.env:
CODEX_SESSION_ID=sess_ваш_idУзнать id: codex session list
Альтернативно — через CLI: bash scripts/codex-state.sh set session_id sess_ваш_id
После этого команды plan и code будут отправлять ревью в эту сессию через resume.
Создание новой сессии
"Используем workflow с codex ревьювером. Задачи: #23, #10"Claude вызывает init — создаётся сессия Codex. По умолчанию init лёгкий (Codex подтверждает готовность). С CODEX_REVIEWER_PROMPT в config.env init выполняет кастомную процедуру (например, исследование кодовой базы). Роль ревьюера задаётся автоматически. Затем plan и code отправляют ревью в эту сессию через resume.
Workflow
1. Init — Claude создаёт сессию Codex (init) 2. Plan Review — Claude описывает план, Codex ревьюит (plan) 3. Implementation — Claude реализует по одобренному плану 4. Code Review — Claude описывает изменения, Codex ревьюит (code) 5. Done — результат пользователю
Управление состоянием
bash scripts/codex-state.sh show # Текущее состояние
bash scripts/codex-state.sh dir # Путь к state-каталогу текущей ветки
bash scripts/codex-state.sh reset # Сброс итераций
bash scripts/codex-state.sh reset --full # Полный сброс
bash scripts/codex-state.sh set session_id <value> # Ручная установка
bash scripts/codex-state.sh set phase implementing # Обновить фазуСтруктура .codex-review/
В корне основного репо (не worktree) создается директория с per-branch изоляцией:
.codex-review/
├── config.env # gitignore — общие настройки проекта
├── .gitkeep
├── archive/ # gitignore — общий архив всех сессий
│ └── {timestamp}/ # артефакты одной сессии (branch в summary.json)
├── feat-auth/ # per-branch state (имя ветки, / → -)
│ ├── state.json # gitignore — транзиентное состояние
│ ├── STATUS.md # gitignore — автогенерируемый статус для Claude
│ ├── verdict.txt # gitignore — последний вердикт от Codex
│ ├── last_response.txt # gitignore — последний ответ Codex
│ ├── codex-init.log # gitignore — лог инициализации сессии
│ ├── codex-{phase}-{N}.log # gitignore — логи итераций ревью
│ └── notes/ # В GIT — журнал текущего ревью для команды
│ ├── .gitkeep
│ ├── plan-review-1.md
│ └── code-review-1.md
└── feat-ui/ # другая ветка — полная изоляция
└── ...CLAUDE.md
Добавь в CLAUDE.md проекта (одноразовая настройка):
## Codex Review
Check for `.codex-review/*/STATUS.md` — if a STATUS.md exists for the current branch, read it before starting work (an active review is in progress).STATUS.md создаётся и обновляется автоматически скриптами плагина в state-каталоге ветки (путь: codex-state.sh dir). Наличие файла означает активное ревью, отсутствие — ревью не идёт или завершено.
Git Worktree Support
The plugin works transparently from git worktrees:
.codex-review/is always resolved to the main repository root viagit rev-parse --git-common-dir- Review state is isolated per branch — each branch gets its own subdirectory (e.g.
.codex-review/feat-auth/) - Multiple worktrees on different branches can run reviews in parallel without conflicts
config.envis shared across all branches (project-level settings)- No additional setup required
Auto-Review Mode
When AUTO_REVIEW=true in .codex-review/config.env, the plugin enforces automated review:
- Plan phase: a plugin hook blocks
ExitPlanModeuntil Codex approves the plan AND the approval was issued in the current Claude session. The hook binds each plan review to the Claude session that ran it, so a stale verdict from a previous task or session cannot silently auto-approve a new plan. If the verdict is missing, stale, not approved, or from a different session, the hook denies the exit and instructs Claude to load the codex-review skill and run plan review first. - Code phase: after implementation, Claude automatically sends code for review and iterates until approved.
No additional configuration needed — the hook is declared in plugin.json and auto-registered when the plugin is enabled.
Анти-рекурсия
Плагин защищен от рекурсивного вызова на 3 уровнях:
1. Env guard — CODEX_REVIEWER=1 при вызове codex exec; если скрипт вызван с этой переменной — exit 1 2. Промпт-контекст — путь к скиллу в промпте для ориентации 3. AGENTS.md — инструкция для Codex о роли ревьюера
#!/bin/sh
# PermissionRequest hook for ExitPlanMode.
#
# When AUTO_REVIEW=true, this hook binds Codex verdict to the current Claude
# session via .codex-review/<branch>/current_session.txt:
#
# - session_id missing from stdin → deny "invalid stdin"
# - current_session.txt missing → claim + deny (untrusted start)
# - current_session.txt mismatches → overwrite + deny (session changed)
# - session matches:
# * no verdict → deny "run plan review"
# * APPROVED → allow + remove verdict
# * CHANGES_REQUESTED → deny "resubmit"
# * unknown value → deny "unknown verdict"
#
# When AUTO_REVIEW!=true: exit silently (normal UI dialog).
set -e
# --- Locate git repo root ---
git_common_dir="$(git rev-parse --git-common-dir 2>/dev/null)" || exit 0
repo_root="$(cd "$git_common_dir/.." && pwd)"
# --- Read config.env ---
# Source in a subshell to match common.sh behavior exactly (handles `export`,
# leading whitespace, quoted values, etc). Side effects stay isolated.
config_file="$repo_root/.codex-review/config.env"
# shellcheck source=/dev/null
AUTO_REVIEW="$( . "$config_file" 2>/dev/null; echo "${AUTO_REVIEW:-false}" )"
# Not auto mode — exit silently (normal UI dialog)
if [ "$AUTO_REVIEW" != "true" ]; then
exit 0
fi
# --- Find branch state dir ---
branch="$(git symbolic-ref --short HEAD 2>/dev/null)" \
|| branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)" \
|| branch="detached"
branch_slug="$(echo "$branch" | tr '/' '-')"
state_dir="$repo_root/.codex-review/$branch_slug"
mkdir -p "$state_dir"
verdict_file="$state_dir/verdict.txt"
session_file="$state_dir/current_session.txt"
# --- Read hook stdin ---
stdin_json="$(cat)"
# --- Parse session_id from stdin JSON ---
# Claude sends a UUID in "session_id". Parse without jq (may not be available
# in the hook execution environment). Match hex+dash chars only so the value
# is safe to interpolate into a JSON string literal.
stdin_session="$(printf '%s' "$stdin_json" \
| grep -oE '"session_id"[[:space:]]*:[[:space:]]*"[0-9a-fA-F-]+"' \
| head -n1 \
| sed -E 's/.*"([0-9a-fA-F-]+)"$/\1/')"
# --- Helpers ---
emit_allow() {
printf '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}\n'
}
# $1: deny message (must be JSON-safe; these are all hardcoded ASCII)
emit_deny() {
printf '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"deny","message":"%s"}}}\n' "$1"
}
# Atomically write session_id to current_session.txt (tmp.$$ → mv on same FS).
claim_session() {
tmp="$session_file.tmp.$$"
printf '%s\n' "$stdin_session" > "$tmp"
mv "$tmp" "$session_file"
}
# --- Validate stdin ---
if [ -z "$stdin_session" ]; then
emit_deny "Invalid hook stdin: missing session_id. This is an internal error; report it to plugin maintainers."
exit 0
fi
# --- Check current session binding ---
if [ ! -f "$session_file" ]; then
# 5A: missing claim → untrusted, purge any orphan verdict and claim session
rm -f "$verdict_file"
claim_session
emit_deny "Codex plan review not claimed for this Claude session. Load skill 'codex-review' and run plan review first: init + plan --plan-file <path>."
exit 0
fi
current_session="$(tr -d '[:space:]' < "$session_file" 2>/dev/null || echo "")"
if [ "$current_session" != "$stdin_session" ]; then
# 5B: session mismatch → another Claude session owned this state, stale
rm -f "$verdict_file"
claim_session
emit_deny "Claude session changed. The previous Codex plan verdict belongs to a different session. Load skill 'codex-review' and re-run plan review for this session: init + plan --plan-file <path>."
exit 0
fi
# --- Session matches: consult verdict.txt ---
if [ ! -f "$verdict_file" ]; then
emit_deny "No Codex plan verdict found. Load skill 'codex-review' and run plan review before ExitPlanMode: init + plan --plan-file <path>."
exit 0
fi
# Read single-word verdict. Strip whitespace, then restrict to [A-Za-z_]
# so the value is safe to interpolate into the JSON deny message below.
verdict="$(tr -d '[:space:]' < "$verdict_file" | tr -cd '[:alpha:]_')"
case "$verdict" in
APPROVED)
rm -f "$verdict_file"
emit_allow
;;
CHANGES_REQUESTED)
emit_deny "Codex plan verdict is CHANGES_REQUESTED. Address feedback and resubmit via 'codex-review.sh plan --plan-file <path>' — do NOT call ExitPlanMode until APPROVED."
;;
*)
[ -n "$verdict" ] || verdict="unknown"
printf '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"deny","message":"Unknown Codex verdict value: %s. Re-run plan review via '\''codex-review.sh plan --plan-file <path>'\''."}}}\n' "$verdict"
;;
esac
#!/bin/bash
# Main codex-review script: init, plan, code
# Usage: codex-review.sh <init|plan|code> <args> [--max-iter N]
# init "task description"
# plan --plan-file <path> (reads file content, passes inline to Codex)
# code "description"
#
# Exit codes:
# 0 — review received (APPROVED or CHANGES_REQUESTED)
# 1 — technical error (codex unavailable, invalid session_id)
# 2 — escalation (max iterations reached)
# 3 — no session (Claude should ask user to create one)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "$SCRIPT_DIR/common.sh"
# --- Anti-recursion (primary defense) ---
guard_recursion
# --- Parse arguments ---
COMMAND="${1:-}"
if [[ -z "$COMMAND" ]]; then
echo "Usage: codex-review.sh <init|plan|code> <args> [--max-iter N]" >&2
exit 1
fi
shift
DESCRIPTION=""
PLAN_FILE=""
MAX_ITER=""
while [[ $# -gt 0 ]]; do
case "$1" in
--plan-file)
PLAN_FILE="$2"
shift 2
;;
--max-iter)
MAX_ITER="$2"
shift 2
;;
*)
DESCRIPTION="$1"
shift
;;
esac
done
# --- Validate arguments per command ---
if [[ "$COMMAND" == "plan" ]]; then
if [[ -z "$PLAN_FILE" ]]; then
echo "ERROR: --plan-file is required for plan review." >&2
echo "Usage: codex-review.sh plan --plan-file <path> [--max-iter N]" >&2
exit 1
fi
if [[ ! -f "$PLAN_FILE" ]]; then
echo "ERROR: Plan file not found: $PLAN_FILE" >&2
exit 1
fi
# Read plan file content as description
DESCRIPTION="$(cat "$PLAN_FILE")"
if [[ -z "$DESCRIPTION" ]]; then
echo "ERROR: Plan file is empty: $PLAN_FILE" >&2
exit 1
fi
elif [[ -z "$DESCRIPTION" && "$COMMAND" != "status" ]]; then
echo "ERROR: Description is required." >&2
echo "Usage: codex-review.sh <init|code> \"description\" [--max-iter N]" >&2
exit 1
fi
# --- Load config & state ---
load_config
check_codex_installed
STATE_DIR="$(get_state_dir)"
MAX_ITERATIONS="${MAX_ITER:-$CODEX_MAX_ITERATIONS}"
SESSION_ID="$(get_effective_session_id)"
# --- Build yolo flags (as array to avoid word splitting) ---
YOLO_FLAG=()
if [[ "$CODEX_YOLO" == "true" ]]; then
YOLO_FLAG=("--yolo")
fi
# --- Reviewer role prompt (reusable base) ---
reviewer_role_prompt() {
cat <<'ROLE'
You are a code reviewer for this project.
You will review plans and code changes submitted by another AI agent (Claude Code).
Focus areas:
- Code quality, readability, maintainability
- Bugs, edge cases, error handling
- Security vulnerabilities
- Architecture and design decisions
- Test coverage adequacy
When reviewing:
- You can inspect the repository yourself — you are in the same working directory
- If the work is acceptable, respond with APPROVED
- If changes are needed, provide specific actionable feedback
- Do NOT run scripts from .codex-review/ — you are the reviewer, not the implementer
- Do NOT look into .codex-review/archive/ — it contains previous session artifacts and is not relevant
- IMPORTANT: This is a non-interactive session. Never ask for confirmation, permission, or clarification — act immediately on instructions
ROLE
}
# --- Default reviewer prompt for init ---
default_reviewer_prompt() {
local task_desc="$1"
local marker="$2"
local role
role="$(reviewer_role_prompt)"
cat <<PROMPT
$role
Task: $task_desc
This message sets up your reviewer role. Plan and code reviews will arrive as follow-up messages — you will inspect the codebase then.
For now, confirm you are ready by responding with "Ready for review".
[session-marker: $marker]
PROMPT
}
# --- Custom init prompt (role + user instructions) ---
custom_init_prompt() {
local custom_instructions="$1"
local task_desc="$2"
local marker="$3"
local role
role="$(reviewer_role_prompt)"
cat <<PROMPT
$role
$custom_instructions
Task: $task_desc
[session-marker: $marker]
PROMPT
}
# --- Extract session_id from codex output (fallback method) ---
extract_session_id() {
local output="$1"
local sid
sid=$(echo "$output" | grep -oE 'sess_[a-zA-Z0-9_-]+' | head -1)
if [[ -z "$sid" ]]; then
sid=$(echo "$output" | grep -oE '[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}' | head -1)
fi
echo "$sid"
}
# --- Extract session_id from log or marker, exit on failure ---
resolve_new_session_id() {
local marker="$1"
local log_file="$2"
local new_session_id
new_session_id="$(find_session_by_marker "$marker")"
if [[ -z "$new_session_id" ]]; then
echo "Marker search failed, trying log regex..." >&2
new_session_id="$(extract_session_id "$(cat "$log_file" 2>/dev/null)")"
fi
if [[ -z "$new_session_id" ]]; then
echo "WARNING: Could not extract session_id." >&2
echo "Log from codex:" >&2
cat "$log_file" >&2
echo "" >&2
echo "Please set session_id manually:" >&2
echo " bash codex-state.sh set session_id <YOUR_SESSION_ID>" >&2
exit 1
fi
echo "$new_session_id"
}
# --- Read verdict from file, fallback to text parsing ---
read_verdict() {
local output="$1"
local verdict_file="$STATE_DIR/verdict.txt"
# Primary: read from verdict file (format-agnostic via helper)
local file_verdict
file_verdict="$(parse_verdict_file "$verdict_file")"
if [[ "$file_verdict" == "APPROVED" || "$file_verdict" == "CHANGES_REQUESTED" ]]; then
echo "$file_verdict"
return
fi
# Fallback: parse response text
if echo "$output" | grep -qiE '(^|\W)APPROVED(\W|$)'; then
echo "APPROVED"
else
echo "CHANGES_REQUESTED"
fi
}
# --- Save review note ---
save_note() {
local phase="$1"
local iteration="$2"
local content="$3"
local note_file="$STATE_DIR/notes/${phase}-review-${iteration}.md"
{
echo "# $(echo "$phase" | awk '{print toupper(substr($0,1,1)) substr($0,2)}') Review #${iteration}"
echo "Date: $(date -u +"%Y-%m-%dT%H:%M:%SZ")"
echo ""
echo "$content"
} > "$note_file"
}
# --- Update state.json ---
update_state() {
local phase="$1"
local iteration="$2"
local status="$3"
local timestamp
timestamp="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
local task_desc
task_desc="$(read_state_field "task_description")"
write_state "{
\"session_id\": \"$SESSION_ID\",
\"phase\": \"$phase\",
\"iteration\": $iteration,
\"max_iterations\": $MAX_ITERATIONS,
\"last_review_status\": \"$status\",
\"last_review_timestamp\": \"$timestamp\",
\"task_description\": \"$task_desc\"
}"
}
# --- Format output ---
print_result() {
local phase="$1"
local iteration="$2"
local max="$3"
local session="$4"
local response="$5"
local status="$6"
echo ""
echo "=== CODEX REVIEW ==="
echo "Phase: $phase"
echo "Iteration: ${iteration}/${max}"
echo "Session: $session"
echo ""
echo "$response"
echo ""
echo "=== END REVIEW ==="
echo "Status: $status"
}
# --- Build phase-specific prompt ---
build_review_prompt() {
local phase="$1"
local description="$2"
local skill_path
skill_path="$(cd "$SCRIPT_DIR/.." && pwd)"
local phase_instructions
if [[ "$phase" == "plan" ]]; then
phase_instructions="You are reviewing a proposed implementation plan.
The full plan text is provided above in 'Description from Claude'. Do NOT read plan files from disk — use the text above as the single source of truth.
Focus areas:
- Correctness: does the approach solve the stated problem?
- Completeness: are requirements and edge cases covered?
- Architecture: are there risks or better alternatives?
- Scope: not too broad, not too narrow?
- Clarity: is the implementation strategy clear and unambiguous?
- Readiness: is the plan specific enough to start coding — are there gaps, undefined decisions, or missing details that would block implementation?"
else
phase_instructions="You are reviewing code changes against the previously approved plan.
Focus areas:
- Plan adherence: does the implementation match the approved plan? Note any deviations or missing parts
- Correctness: bugs, edge cases, off-by-one errors
- Security: injection, auth, data exposure vulnerabilities
- Error handling: failure modes, missing validations
- Code quality: readability, maintainability, naming, structure
- Tests: are critical paths covered? Are tests meaningful, not just nominal?
- Merge readiness: is this code ready to merge as-is, or are there blockers?"
fi
local guide=""
if [[ "$phase" == "plan" ]]; then
guide="$CODEX_PLAN_GUIDE"
else
guide="$CODEX_CODE_GUIDE"
fi
local guide_section=""
if [[ -n "$guide" ]]; then
guide_section="
Additional review guidance from project maintainer:
$guide
"
fi
cat <<PROMPT
You are reviewing work by Claude Code on this project.
Phase: $phase
Description from Claude:
$description
$phase_instructions
$guide_section
General instructions:
- If acceptable, respond with APPROVED
- If changes needed, provide specific actionable feedback
- You can inspect the code yourself — you're in the same directory
- The codex-review skill is at: $skill_path
After your review, write your verdict to $STATE_DIR/verdict.txt
Write exactly one word: APPROVED or CHANGES_REQUESTED
The directory exists. The file is cleared before each review — always create it fresh.
PROMPT
}
# =====================
# COMMAND: init
# =====================
cmd_init() {
local task_desc="$DESCRIPTION"
# Archive previous session artifacts
archive_previous_session
# Clear verdict to prevent stale auto-approve (AUTO_REVIEW hook)
rm -f "$STATE_DIR/verdict.txt"
# Warn if config.env already has a session
if [[ -n "${CODEX_SESSION_ID:-}" ]]; then
echo "WARNING: CODEX_SESSION_ID is already set in config.env: $CODEX_SESSION_ID" >&2
echo "Init will create a NEW session. Update config.env afterwards or remove CODEX_SESSION_ID to use state.json." >&2
fi
# Generate marker for session identification
local marker
marker="$(generate_uuid)"
# Build reviewer prompt
local prompt
if [[ -n "$CODEX_REVIEWER_PROMPT" ]]; then
prompt="$(custom_init_prompt "$CODEX_REVIEWER_PROMPT" "$task_desc" "$marker")"
else
prompt="$(default_reviewer_prompt "$task_desc" "$marker")"
fi
local output_file="$STATE_DIR/last_response.txt"
local log_file="$STATE_DIR/codex-init.log"
echo "Creating Codex session..." >&2
printf '\033[1;33m>>> Monitor: tail -f %s\033[0m\n' "$log_file" >&2
local MODEL_FLAG=()
if [[ -n "$CODEX_MODEL" ]]; then
MODEL_FLAG=("--model" "$CODEX_MODEL")
fi
CODEX_REVIEWER=1 codex exec \
"${MODEL_FLAG[@]}" \
"${YOLO_FLAG[@]}" \
-o "$output_file" \
"$prompt" </dev/null > "$log_file" 2>&1 || {
echo "ERROR: Failed to create Codex session." >&2
cat "$log_file" >&2
exit 1
}
# Extract session_id
SESSION_ID="$(resolve_new_session_id "$marker" "$log_file")"
write_state "{
\"session_id\": \"$SESSION_ID\",
\"phase\": \"initialized\",
\"iteration\": 0,
\"max_iterations\": $MAX_ITERATIONS,
\"last_review_status\": \"\",
\"last_review_timestamp\": \"$(date -u +"%Y-%m-%dT%H:%M:%SZ")\",
\"task_description\": \"$task_desc\"
}"
write_status
echo "Session created: $SESSION_ID"
}
# =====================
# COMMAND: plan / code
# =====================
cmd_review() {
local phase="$1"
# Check session exists
if [[ -z "$SESSION_ID" ]]; then
echo ""
echo "=== CODEX REVIEW ==="
echo "Phase: $phase"
echo ""
echo "No active Codex session found."
echo ""
echo "=== END REVIEW ==="
echo "Status: NO_SESSION"
exit 3
fi
# Reset iteration counter on phase change (e.g. plan → code)
local previous_phase
previous_phase="$(read_state_field "phase")"
if [[ -n "$previous_phase" && "$previous_phase" != "$phase" ]]; then
local task_desc
task_desc="$(read_state_field "task_description")"
write_state "{
\"session_id\": \"$SESSION_ID\",
\"phase\": \"$previous_phase\",
\"iteration\": 0,
\"max_iterations\": $MAX_ITERATIONS,
\"last_review_status\": \"\",
\"last_review_timestamp\": \"$(date -u +"%Y-%m-%dT%H:%M:%SZ")\",
\"task_description\": \"$task_desc\"
}"
echo "Phase changed ($previous_phase → $phase), iteration counter reset." >&2
fi
# Check iteration limit
local current_iteration
current_iteration="$(read_state_number "iteration")"
local next_iteration=$((current_iteration + 1))
if [[ $next_iteration -gt $MAX_ITERATIONS ]]; then
echo ""
echo "=== CODEX REVIEW ==="
echo "Phase: $phase"
echo "Iteration: ${next_iteration}/${MAX_ITERATIONS}"
echo "Session: $SESSION_ID"
echo ""
echo "Maximum iterations ($MAX_ITERATIONS) reached."
echo "Review notes are in: $STATE_DIR/notes/"
echo ""
echo "=== END REVIEW ==="
echo "Status: ESCALATE"
exit 2
fi
# Save plan file copy for history
if [[ "$phase" == "plan" && -n "$PLAN_FILE" ]]; then
cp "$PLAN_FILE" "$STATE_DIR/plan.md"
echo "Plan saved to: $STATE_DIR/plan.md" >&2
fi
local codex_prompt
codex_prompt="$(build_review_prompt "$phase" "$DESCRIPTION")"
# Clean previous verdict before calling codex
rm -f "$STATE_DIR/verdict.txt"
# Call codex with resume
local output_file="$STATE_DIR/last_response.txt"
local log_file="$STATE_DIR/codex-${phase}-${next_iteration}.log"
echo "Sending $phase for review (iteration ${next_iteration}/${MAX_ITERATIONS})..." >&2
printf '\033[1;33m>>> Monitor: tail -f %s\033[0m\n' "$log_file" >&2
local MODEL_FLAG=()
if [[ -n "$CODEX_MODEL" ]]; then
MODEL_FLAG=("--model" "$CODEX_MODEL")
fi
local REASONING_FLAG=()
if [[ -n "$CODEX_REASONING_EFFORT" ]]; then
REASONING_FLAG=("-c" "model_reasoning_effort=\"$CODEX_REASONING_EFFORT\"")
fi
CODEX_REVIEWER=1 codex exec \
"${MODEL_FLAG[@]}" \
"${REASONING_FLAG[@]}" \
"${YOLO_FLAG[@]}" \
-o "$output_file" \
resume "$SESSION_ID" \
"$codex_prompt" </dev/null > "$log_file" 2>&1 || {
local exit_code=$?
echo "ERROR: Codex exec failed (exit $exit_code)." >&2
cat "$log_file" >&2
update_state "$phase" "$next_iteration" "ERROR"
exit 1
}
local output
output=$(cat "$output_file" 2>/dev/null || echo "")
# Read verdict (file → fallback to text parsing)
local status
status="$(read_verdict "$output")"
# Save note
save_note "$phase" "$next_iteration" "$output"
# Update state
update_state "$phase" "$next_iteration" "$status"
# Update or remove STATUS.md
if [[ "$phase" == "code" && "$status" == "APPROVED" ]]; then
remove_status
else
write_status
fi
# Print result
print_result "$phase" "$next_iteration" "$MAX_ITERATIONS" "$SESSION_ID" "$output" "$status"
}
# --- Main ---
case "$COMMAND" in
init) cmd_init ;;
plan) cmd_review "plan" ;;
code) cmd_review "code" ;;
*)
echo "Usage: codex-review.sh <init|plan|code> <args> [--max-iter N]" >&2
echo "" >&2
echo "Commands:" >&2
echo " init \"task\" Create a new Codex session for the given task" >&2
echo " plan --plan-file <path> Submit plan for review (reads file, passes inline)" >&2
echo " code \"description\" Submit code for review" >&2
echo "" >&2
echo "Exit codes:" >&2
echo " 0 — Review received (APPROVED or CHANGES_REQUESTED)" >&2
echo " 1 — Technical error" >&2
echo " 2 — Escalation (max iterations)" >&2
echo " 3 — No session" >&2
exit 1
;;
esac
#!/bin/bash
# State management for codex-review plugin
# Usage: codex-state.sh {show|reset|get|set} [args]
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "$SCRIPT_DIR/common.sh"
STATE_DIR="$(get_state_dir)"
STATE_FILE="$STATE_DIR/state.json"
cmd_show() {
local effective_sid
effective_sid="$(get_effective_session_id)"
if [[ -f "$STATE_FILE" ]]; then
# Replace session_id in output with effective value (config.env takes priority)
sed "s|\"session_id\"[[:space:]]*:[[:space:]]*\"[^\"]*\"|\"session_id\": \"$effective_sid\"|" "$STATE_FILE"
else
echo "{\"session_id\":\"$effective_sid\",\"phase\":\"\",\"iteration\":0,\"max_iterations\":3,\"last_review_status\":\"\",\"last_review_timestamp\":\"\",\"task_description\":\"\"}"
fi
}
cmd_reset() {
if [[ "${1:-}" == "--full" ]]; then
archive_previous_session
mkdir -p "$STATE_DIR/notes"
touch "$STATE_DIR/notes/.gitkeep"
echo "Full reset complete."
else
local session_id task_desc
session_id="$(get_effective_session_id)"
task_desc="$(read_state_field "task_description")"
write_state "{
\"session_id\": \"$session_id\",
\"phase\": \"\",
\"iteration\": 0,
\"max_iterations\": $CODEX_MAX_ITERATIONS,
\"last_review_status\": \"\",
\"last_review_timestamp\": \"\",
\"task_description\": \"$task_desc\"
}"
write_status
echo "Reset complete (session_id preserved)."
fi
}
cmd_get() {
local field="${1:?Usage: codex-state.sh get <field>}"
if [[ "$field" == "session_id" ]]; then
get_effective_session_id
return
fi
if [[ "$field" == "verdict" ]]; then
parse_verdict_file "$STATE_DIR/verdict.txt"
return
fi
local val
val="$(read_state_field "$field")"
if [[ -z "$val" ]]; then
val="$(read_state_number "$field")"
fi
echo "$val"
}
cmd_set() {
local field="${1:?Usage: codex-state.sh set <field> <value>}"
local value="${2:?Usage: codex-state.sh set <field> <value>}"
if [[ ! -f "$STATE_FILE" ]]; then
write_state "{
\"session_id\": \"\",
\"phase\": \"\",
\"iteration\": 0,
\"max_iterations\": 3,
\"last_review_status\": \"\",
\"last_review_timestamp\": \"\",
\"task_description\": \"\"
}"
fi
local tmp
tmp=$(sed "s|\"$field\"[[:space:]]*:[[:space:]]*\"[^\"]*\"|\"$field\": \"$value\"|" "$STATE_FILE")
echo "$tmp" > "$STATE_FILE"
write_status
echo "Set $field = $value"
}
# --- Load config for defaults ---
load_config
# --- Main ---
case "${1:-}" in
show) cmd_show ;;
dir) echo "$STATE_DIR" ;;
reset) cmd_reset "${2:-}" ;;
get) cmd_get "${2:-}" ;;
set) cmd_set "${2:-}" "${3:-}" ;;
*)
echo "Usage: codex-state.sh {show|reset|dir|get|set} [args]"
echo " show Current state (JSON)"
echo " dir Print state directory path for current branch"
echo " reset Reset iterations/phase (keep session_id)"
echo " reset --full Full reset + delete notes"
echo " get <field> Get a single field (special: 'verdict' reads verdict.txt)"
echo " set <field> <val> Set a field (e.g. session_id)"
exit 1
;;
esac
#!/bin/bash
# Common functions for codex-review plugin
# shellcheck disable=SC2034
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# --- Anti-recursion guard (deterministic, primary defense) ---
guard_recursion() {
if [[ "${CODEX_REVIEWER:-}" == "1" ]]; then
echo "ERROR: Recursion detected (CODEX_REVIEWER=1). Aborting." >&2
exit 1
fi
}
# --- Project root via git (current worktree or main repo) ---
get_project_root() {
git rev-parse --show-toplevel 2>/dev/null || {
echo "ERROR: Not inside a git repository." >&2
exit 1
}
}
# --- Main repo root (resolves through worktrees to the original repo) ---
# In a worktree, --show-toplevel returns the worktree root, but .codex-review/
# only exists in the main repo (it's excluded from git). This function always
# returns the main repo root so state files are found regardless of context.
get_main_repo_root() {
local git_common_dir
git_common_dir="$(git rev-parse --git-common-dir 2>/dev/null)" || {
echo "ERROR: Not inside a git repository." >&2
exit 1
}
# --git-common-dir returns the .git dir of the main repo:
# - in main repo: ".git" (relative)
# - in worktree: "/abs/path/to/main/.git" (absolute)
# Parent of .git dir is the repo root in both cases.
(cd "$git_common_dir/.." && pwd)
}
# --- Current branch name, sanitized for use as directory name ---
get_branch_slug() {
local branch
branch="$(git symbolic-ref --short HEAD 2>/dev/null)" \
|| branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)" \
|| branch="detached"
# Replace slashes with dashes: feat/auth/jwt → feat-auth-jwt
echo "$branch" | tr '/' '-'
}
# --- Root .codex-review/ directory (shared config, per-branch subdirs) ---
get_review_root() {
local root
root="$(get_main_repo_root)"
local review_root="$root/.codex-review"
mkdir -p "$review_root"
touch "$review_root/.gitkeep"
echo "$review_root"
}
# --- State directory (per-branch isolation inside .codex-review/) ---
get_state_dir() {
local review_root
review_root="$(get_review_root)"
local branch
branch="$(get_branch_slug)"
local state_dir="$review_root/$branch"
mkdir -p "$state_dir/notes"
touch "$state_dir/notes/.gitkeep"
echo "$state_dir"
}
# --- Load config (shared config.env → env vars → defaults) ---
load_config() {
local review_root
review_root="$(get_review_root)"
local config_file="$review_root/config.env"
if [[ -f "$config_file" ]]; then
# shellcheck disable=SC1090
source "$config_file"
fi
CODEX_MODEL="${CODEX_MODEL:-}"
CODEX_REASONING_EFFORT="${CODEX_REASONING_EFFORT:-}"
CODEX_MAX_ITERATIONS="${CODEX_MAX_ITERATIONS:-5}"
CODEX_YOLO="${CODEX_YOLO:-true}"
AUTO_REVIEW="${AUTO_REVIEW:-false}"
CODEX_REVIEWER_PROMPT="${CODEX_REVIEWER_PROMPT:-}"
CODEX_PLAN_GUIDE="${CODEX_PLAN_GUIDE:-}"
CODEX_CODE_GUIDE="${CODEX_CODE_GUIDE:-}"
}
# --- Read a field from state.json (no jq dependency) ---
read_state_field() {
local field="$1"
local state_dir
state_dir="$(get_state_dir)"
local state_file="$state_dir/state.json"
if [[ ! -f "$state_file" ]]; then
echo ""
return
fi
grep -o "\"$field\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" "$state_file" \
| head -1 \
| sed 's/.*:[[:space:]]*"//' \
| tr -d '"'
}
# --- Read numeric field from state.json ---
read_state_number() {
local field="$1"
local state_dir
state_dir="$(get_state_dir)"
local state_file="$state_dir/state.json"
if [[ ! -f "$state_file" ]]; then
echo "0"
return
fi
local val
val=$(grep -o "\"$field\"[[:space:]]*:[[:space:]]*[0-9]*" "$state_file" \
| head -1 \
| sed 's/.*:[[:space:]]*//')
echo "${val:-0}"
}
# --- Effective session_id: config.env → state.json ---
get_effective_session_id() {
local sid="${CODEX_SESSION_ID:-}"
if [[ -z "$sid" ]]; then
sid="$(read_state_field "session_id")"
fi
echo "$sid"
}
# --- Write state.json ---
write_state() {
local json="$1"
local state_dir
state_dir="$(get_state_dir)"
echo "$json" > "$state_dir/state.json"
}
# --- Write STATUS.md from current state.json ---
write_status() {
local state_dir
state_dir="$(get_state_dir)"
local status_file="$state_dir/STATUS.md"
local task phase iteration max_iter review_status
task="$(read_state_field "task_description")"
phase="$(read_state_field "phase")"
iteration="$(read_state_number "iteration")"
max_iter="$(read_state_number "max_iterations")"
review_status="$(read_state_field "last_review_status")"
local branch
branch="$(get_branch_slug)"
{
echo "# Active Codex Review"
echo "- Task: ${task:-not set}"
echo "- Branch: ${branch}"
echo "- Phase: ${phase:-initialized}"
echo "- Iteration: ${iteration}/${max_iter}"
echo "- Last status: ${review_status:-pending}"
echo "- Journal: \`.codex-review/${branch}/notes/\`"
} > "$status_file"
}
# --- Remove STATUS.md (review complete or full reset) ---
remove_status() {
local state_dir
state_dir="$(get_state_dir)"
rm -f "$state_dir/STATUS.md"
}
# --- Parse verdict file ---
# Reads single-word verdict file and prints normalized verdict string.
# Output: "APPROVED", "CHANGES_REQUESTED", or empty for missing/unknown.
parse_verdict_file() {
local file="$1"
[[ -f "$file" ]] || return 0
local raw
raw="$(tr -d '[:space:]' < "$file")"
case "$raw" in
APPROVED|CHANGES_REQUESTED) echo "$raw" ;;
*) : ;;
esac
}
# --- Archive previous session artifacts ---
archive_previous_session() {
local state_dir
state_dir="$(get_state_dir)"
local review_root
review_root="$(get_review_root)"
local has_artifacts=false
# Check if there's anything to archive
for f in "$state_dir"/state.json "$state_dir"/verdict.txt "$state_dir"/last_response.txt "$state_dir"/STATUS.md; do
if [[ -f "$f" ]]; then has_artifacts=true; break; fi
done
if ls "$state_dir"/notes/*.md &>/dev/null; then has_artifacts=true; fi
if ls "$state_dir"/codex-*.log &>/dev/null; then has_artifacts=true; fi
if [[ "$has_artifacts" == "false" ]]; then
return
fi
local timestamp
timestamp="$(date -u +"%Y%m%dT%H%M%SZ")"
local archive_dir="$review_root/archive/${timestamp}"
mkdir -p "$archive_dir/notes"
# Generate summary.json before moving artifacts (non-critical, must not block archiving)
generate_archive_summary "$state_dir" "$archive_dir" "$timestamp" || \
echo "WARNING: Failed to generate summary.json for archive." >&2
# Move artifacts
for f in state.json verdict.txt last_response.txt STATUS.md; do
[[ -f "$state_dir/$f" ]] && mv "$state_dir/$f" "$archive_dir/"
done
mv "$state_dir"/codex-*.log "$archive_dir/" 2>/dev/null || true
mv "$state_dir"/notes/*.md "$archive_dir/notes/" 2>/dev/null || true
echo "Previous session archived to: $archive_dir" >&2
}
# --- Generate summary.json for archive ---
generate_archive_summary() {
local state_dir="$1"
local archive_dir="$2"
local archived_at="$3"
local task_desc="" session_id="" final_verdict="" last_status=""
local plan_iters=0 code_iters=0
# Read from state.json (still in state_dir at this point)
if [[ -f "$state_dir/state.json" ]]; then
task_desc="$(grep -o '"task_description"[[:space:]]*:[[:space:]]*"[^"]*"' "$state_dir/state.json" \
| head -1 | sed 's/.*:[[:space:]]*"//;s/"$//')"
session_id="$(grep -o '"session_id"[[:space:]]*:[[:space:]]*"[^"]*"' "$state_dir/state.json" \
| head -1 | sed 's/.*:[[:space:]]*"//;s/"$//')"
last_status="$(grep -o '"last_review_status"[[:space:]]*:[[:space:]]*"[^"]*"' "$state_dir/state.json" \
| head -1 | sed 's/.*:[[:space:]]*"//;s/"$//')"
fi
# Read final verdict via format-agnostic helper
final_verdict="$(parse_verdict_file "$state_dir/verdict.txt")"
if [[ -z "$final_verdict" ]]; then
final_verdict="$last_status"
fi
# Count review iterations from notes
# shellcheck disable=SC2012
plan_iters=$(ls "$state_dir"/notes/plan-review-*.md 2>/dev/null | wc -l)
# shellcheck disable=SC2012
code_iters=$(ls "$state_dir"/notes/code-review-*.md 2>/dev/null | wc -l)
local total_iters=$((plan_iters + code_iters))
# Escape task_desc for JSON (replace " with \", newlines with \n)
task_desc="$(echo "$task_desc" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' ' ')"
local branch
branch="$(get_branch_slug)"
cat > "$archive_dir/summary.json" <<SUMMARY_EOF
{
"branch": "$branch",
"task_description": "$task_desc",
"session_id": "$session_id",
"plan_iterations": $plan_iters,
"code_iterations": $code_iters,
"total_iterations": $total_iters,
"final_verdict": "$final_verdict",
"archived_at": "$archived_at"
}
SUMMARY_EOF
}
# --- Generate UUID ---
generate_uuid() {
cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen 2>/dev/null || {
# Last resort: pseudo-random hex
od -x /dev/urandom 2>/dev/null | head -1 | awk '{print $2$3"-"$4"-"$5"-"$6"-"$7$8$9}'
}
}
# --- Codex sessions directory for today ---
get_sessions_dir() {
local codex_home="${CODEX_HOME:-$HOME/.codex}"
local today
today="$(date -u +%Y/%m/%d)"
echo "$codex_home/sessions/$today"
}
# --- Find session_id by marker UUID in today's session files ---
find_session_by_marker() {
local marker="$1"
local sessions_dir
sessions_dir="$(get_sessions_dir)"
if [[ ! -d "$sessions_dir" ]]; then
echo ""
return
fi
local found_file
found_file=$(grep -rl "$marker" "$sessions_dir"/ 2>/dev/null | head -1)
if [[ -z "$found_file" ]]; then
echo ""
return
fi
# Primary: read session_meta.payload.id from first line via jq
if command -v jq &>/dev/null; then
local sid
sid=$(head -1 "$found_file" | jq -r '.payload.id // empty' 2>/dev/null)
if [[ -n "$sid" ]]; then
echo "$sid"
return
fi
fi
# Fallback: extract UUID via grep from first line (no jq)
local sid
sid=$(head -1 "$found_file" | grep -oE '"id":"[^"]+"' | head -1 | sed 's/"id":"//;s/"//')
echo "$sid"
}
# --- Check codex is installed and not blocked by sandbox ---
check_codex_installed() {
if ! command -v codex &>/dev/null; then
echo "ERROR: 'codex' CLI not found in PATH." >&2
echo "Install: npm install -g @openai/codex" >&2
exit 1
fi
# Preflight: detect sandbox crash (macOS SCDynamicStore panic)
local ver_output
ver_output=$(codex --version 2>&1) || true
if echo "$ver_output" | grep -q "SCDynamicStore\|Attempted to create a NULL object\|dynamic_store\.rs"; then
echo "ERROR: codex CLI crashes due to sandbox restrictions (macOS SCDynamicStore)." >&2
echo "Claude Code must run codex-review scripts with dangerouslyDisableSandbox: true." >&2
echo "This is a known issue: the sandbox blocks macOS system configuration APIs that codex needs." >&2
exit 1
fi
}
Related skills
FAQ
What does codex-review require to run on macOS?
All codex-review.sh and codex-state.sh calls must run with dangerouslyDisableSandbox true, because Codex CLI uses macOS system APIs blocked by the Claude Code sandbox.
What gets reviewed?
Both the plan (ideally before ExitPlanMode) and the implemented code, so it passes technical review by Codex and business-priority review by the user.