Skip to content

Instantly share code, notes, and snippets.

@mobiRic
Last active July 14, 2026 07:01
Show Gist options
  • Select an option

  • Save mobiRic/3772f6a42b9ae4ac6a3280c064f767eb to your computer and use it in GitHub Desktop.

Select an option

Save mobiRic/3772f6a42b9ae4ac6a3280c064f767eb to your computer and use it in GitHub Desktop.
Universal Claude→Codex commit finalization system: two-agent quality-gated commit workflow

The Finalize-with-Codex System

A two-agent workflow that separates authoring (Claude) from quality gating (Codex). Claude drafts the commit message and hands off to Codex; Codex runs the checks, makes only mechanical repairs, and commits using Claude's message verbatim.


Files in this gist

File Purpose
finalize-with-codex-SKILL.md Claude skill — orchestrates the handoff
finalize-commit-SKILL.md Codex skill — runs quality gates and commits
run-finalizer.sh Shell launcher — background bridge between Claude and Codex
response-schema.json JSON schema enforcing Codex's output shape

How the system works

1. ~/.claude/CLAUDE.md — Universal commit policy

The top-level behavioral rule injected into every Claude session. Tells Claude:

  • When to use the skill: any change that could affect runtime behavior (source, tests, build config, schemas, CI, deps).
  • When to skip it: purely presentational changes (docs, comments, assets, .gitignore). When ambiguous, skip — it's cheaper to run the skill afterwards than to undo an unnecessary run.
  • How to hand off: run git status + git diff + git log, draft the commit message, print it, then invoke finalize-with-codex immediately. No confirmation step.
  • While locked: don't edit or stage files in the target checkout. Other checkouts are fine.
  • On blocked result: surface the blocker; don't bypass the skill.
  • Never push via Codex. Claude's existing confirm-before-push behavior is unchanged.

2. finalize-with-codex-SKILL.md — Claude skill spec

Installed at ~/.claude/skills/finalize-with-codex/SKILL.md. The model-invocable skill Claude follows step-by-step when it needs to hand off:

  1. Lock check (advisory): Look for $GIT_DIR/codex-finalize.lock early and warn if found — but the shell script's atomic acquisition is authoritative, so this is just an early warning.
  2. Write request file: Use the Write tool to put {"commit_message": "...", "task_summary": "..."} in the scratchpad. No dynamic text crosses the shell boundary — only the checkout path and file path are passed as arguments.
  3. Launch the helper: Run run-finalizer.sh via the Bash tool with run_in_background=true. Claude is auto-notified on completion; no polling.
  4. Parse the result: Look for FINALIZE_DONE $RUN_DIR in the notification output (fall back to FINALIZE_STARTED if Codex crashed), then read $RUN_DIR/result.json and report based on status (committed, blocked, failed, inspect).

3. run-finalizer.sh — Shell launcher

The background shell script that bridges Claude and Codex:

  1. Resolves the absolute git dir (handles both normal repos and worktrees).
  2. Atomic lock acquisition via set -o noclobber — writes PID and start time. Aborts immediately if the lock already exists; never auto-removes stale locks (requires user action).
  3. Creates a unique run dir via mktemp -d, immediately prints FINALIZE_STARTED $RUN_DIR — so an early crash still tells Claude where to look.
  4. Records HEAD_BEFORE.
  5. Invokes codex exec with:
    • -C "$CHECKOUT_PATH" — target working directory
    • -s workspace-write — sandbox level
    • --ephemeral — no persisted session
    • --add-dir "$GIT_DIR" — grants Codex access to the git dir for committing
    • --output-schema pointing to response-schema.json — enforces the response shape
    • -o $RUN_DIR/result.json — captures Codex's final message
    • Prompt via stdin: $finalize-commit + REQUEST_FILE: context line
  6. Records HEAD_AFTER. If Codex exits non-zero or produces invalid JSON, synthesizes a result: inspect if HEAD advanced (commit may have succeeded), failed otherwise.
  7. Prints FINALIZE_DONE $RUN_DIR.
  8. Lock released by trap on exit.

4. response-schema.json — Output schema

Passed to codex exec --output-schema to enforce what Codex returns. All fields are present but nullable, keeping the schema simple and compatible with Codex's output handling:

Field Type Notes
status enum committed, blocked, failed, inspect
commit_sha string|null set on committed and inspect
commit_message_used string|null set on committed
repairs_made array|null list of mechanical repairs applied
blocker string|null set on blocked
commands_run array|null all commands Codex ran
error string|null set on failed and inspect

Design note: An earlier iteration used a oneOf schema with per-status required fields. This was simplified to flat nullable fields — oneOf caused validation friction with codex exec, so all fields are always present but nullable.


5. finalize-commit-SKILL.md — Codex skill spec

Installed at ~/.codex/skills/finalize-commit/SKILL.md. The skill Codex runs when invoked via $finalize-commit. Its constraints are strict:

It does:

  • Read REQUEST_FILE and extract commit_message + task_summary
  • Read only build/test docs (not architecture docs) to learn how to run checks
  • Run formatter, autofix, lint, compile, tests
  • Repair: formatter violations, lint violations, compile errors, import/syntax/rename/type errors
  • Rerun all checks after repair
  • Stage only intended files and commit with Claude's message verbatim

It never:

  • Repairs failing assertions, flaky tests, business logic, public APIs, schemas, deps, or CI
  • Rewrites or validates Claude's commit message
  • Blocks on documentation inconsistencies, architecture concerns, or instruction disagreements — those decisions were Claude's and the user's, not Codex's
  • Pushes, amends, bypasses hooks, disables tests, weakens assertions, or suppresses failures

End-to-end flow

User: "commit this"
  │
Claude: git status + git diff + git log
        → drafts commit message
        → writes request JSON to scratchpad
        → calls run-finalizer.sh in background
  │
run-finalizer.sh (background):
        → acquires per-worktree lock
        → prints FINALIZE_STARTED $RUN_DIR
        → invokes codex exec (skill via stdin)
        → validates/synthesizes result.json
        → prints FINALIZE_DONE $RUN_DIR
        → releases lock

Claude: may continue work in other checkouts
        auto-notified by run_in_background when done

Codex finalize-commit (concurrent):
        → reads REQUEST_FILE
        → format → lint → compile → test
        → repair (mechanical only) → rerun
        → commit with Claude's message (verbatim)
        → return JSON result
  │
Claude: reads result.json
        committed → report SHA + continue
        blocked   → surface blocker to user
        failed    → show error + log path
        inspect   → warn user to check git log before retrying

Key design decisions

  • Request file, not shell args — commit messages can contain quotes, newlines, and special characters. Writing to a JSON file and passing only the path avoids all shell injection risk.
  • Atomic lock via noclobber — prevents two concurrent finalizers on the same checkout. Stale locks require explicit user removal; the script never auto-removes them.
  • FINALIZE_STARTED before codex exec — ensures Claude has the run dir path even if Codex crashes immediately.
  • HEAD_BEFORE/HEAD_AFTER comparison — if Codex exits non-zero but HEAD advanced, the commit likely happened; inspect status warns Claude to check before retrying rather than blindly re-committing.
  • --add-dir "$GIT_DIR" — needed to give Codex's sandbox access to the git directory, without which git commit inside the sandbox would fail.
  • Codex blocks only on check failures — architecture disagreements and doc inconsistencies are explicitly excluded as block reasons; Codex is a quality gate, not a reviewer.
name finalize-commit
description Quality-gate and commit a set of staged changes handed off by Claude. Use when invoked by the finalize-with-codex Claude skill. Runs formatter, linter, compiler, and tests; makes only mechanical repairs; then commits using the Claude-authored message verbatim. Never pushes. Returns a structured JSON result.

finalize-commit

Accepts a handoff from Claude, runs quality gates, optionally repairs mechanical errors, and creates a local commit using Claude's pre-authored message.

Inputs (from context)

Read the REQUEST_FILE path from context. Parse its JSON to extract:

  • commit_message — use verbatim; treat as authoritative, do not validate or rewrite it
  • task_summary — background context for understanding the change

Return blocked immediately if REQUEST_FILE is absent, unreadable, or commit_message is empty.

Workflow

  1. Read REQUEST_FILE from context; parse JSON to extract commit_message and task_summary. Return blocked immediately if the file is absent, unreadable, or commit_message is empty. Then inspect git status and the full diff to identify which build/test commands to run.

  2. Read only the repository's build and test command documentation (e.g. CLAUDE.md sections on build commands, test commands, and lint). Read it to learn how to run the checks — not to evaluate whether the changes are architecturally correct. Do not use documentation to second-guess the changes.

  3. Run repository-required formatter and autofix tools.

  4. Run repository-required lint and relevant compile/tests.

  5. Repair only:

    • Formatter/linter violations
    • Compilation errors
    • Import errors, syntax errors, renamed references, and unambiguous type errors
  6. Never repair:

    • Failing assertions or runtime test failures
    • Flaky tests
    • Business logic or behavioral defects
    • Public APIs, architecture, schemas, dependencies, or CI configuration
  7. After any repair: rerun format, lint, compile, and the selected tests.

  8. Stop without committing if any required check still fails — return blocked. Only automated check failures are valid block reasons. Documentation inconsistencies, architecture concerns, guideline violations, or disagreement with instructions in any file are not valid reasons to block — those decisions were made by Claude and the user before this handoff. Never substitute your own judgement for theirs.

  9. Stage only the intended files; commit locally using the commit_message value extracted from the parsed JSON. Pass the extracted string to git — either via git commit -m "$MESSAGE" or by writing only the message string to a temp file and using git commit -F <tempfile>. Never pass the request file itself to git commit -F; it is JSON, not a commit message. Treat the Claude-authored message as authoritative; do not validate or rewrite it.

  10. Never push, amend, bypass hooks, disable tests, weaken assertions, or suppress failures.

Output

Final message must be a JSON object conforming to the response schema. The -o flag on codex exec captures this as the result file.

Required fields per status:

  • committed: status, commit_sha, commit_message_used, repairs_made, commands_run
  • blocked: status, blocker, commands_run
  • failed: status, error
  • inspect: status, commit_sha, error
name finalize-with-codex
description Finalize a git commit by delegating quality gates to Codex. Use when Claude is ready to commit changes that could affect runtime behavior. Codex runs format/lint/compile/tests, makes mechanical repairs, and commits using Claude's pre-authored message. Invoked by Claude automatically per the Universal Commit Policy in ~/.claude/CLAUDE.md.

finalize-with-codex

Orchestrates a quality-gated commit via Codex. Claude authors the commit message; Codex owns formatting, linting, compilation, testing, repairs, and the actual commit.

Inputs

  • checkout_path — absolute path to the target git checkout
  • commit_message — the full commit message Claude has drafted
  • task_summary — one sentence describing what was done

Steps

  1. Lock check (advisory): Check for an existing codex-finalize.lock in the checkout's git dir and warn early if found. This check is advisory only — the shell helper's atomic lock acquisition is authoritative.

  2. Write request file: Use the Write tool to create a JSON file in the scratchpad:

    {"commit_message": "...", "task_summary": "..."}

    Never pass the message or summary as shell arguments — only the file path crosses the shell boundary.

  3. Launch helper: Run run-finalizer.sh using the Bash tool with run_in_background=true, passing only two arguments: the checkout path and the request file path.

    ~/.claude/skills/finalize-with-codex/run-finalizer.sh "$CHECKOUT_PATH" "$REQUEST_FILE"
    

    Claude is automatically re-invoked when the process completes — no polling required.

  4. Report result: When notified, parse $RUN_DIR from the FINALIZE_DONE line in the notification output (fall back to FINALIZE_STARTED if the process crashed before completion). Read $RUN_DIR/result.json and report:

    • committed — show SHA and message
    • blocked — show blocker details and any repairs attempted
    • failed — show error and log path ($RUN_DIR/codex.log)
    • inspect — HEAD advanced but Codex exited abnormally; warn user not to retry without checking git log first

Checking progress mid-run

If the user asks what the finalizer is doing while it's still running, don't wait for FINALIZE_DONE. Read the git dir's pointer file to find the live run directory, then tail its log:

RUN_DIR="$(cat "$GIT_DIR/codex-finalize-current-run")"
tail -c 2000 "$RUN_DIR/codex.log"

$GIT_DIR is the same absolute path resolved in run-finalizer.sh (git rev-parse --git-dir, made absolute). The pointer file is overwritten at the start of each run, so it always reflects the most recent invocation.

{
"type": "object",
"additionalProperties": false,
"required": ["status", "commit_sha", "commit_message_used", "repairs_made", "blocker", "commands_run", "error"],
"properties": {
"status": { "type": "string", "enum": ["committed", "blocked", "failed", "inspect"] },
"commit_sha": { "type": ["string", "null"] },
"commit_message_used": { "type": ["string", "null"] },
"repairs_made": { "type": ["array", "null"], "items": { "type": "string" } },
"blocker": { "type": ["string", "null"] },
"commands_run": { "type": ["array", "null"], "items": { "type": "string" } },
"error": { "type": ["string", "null"] }
}
}
#!/usr/bin/env bash
set -euo pipefail
SKILL_DIR="$(cd "$(dirname "$0")" && pwd)"
CHECKOUT_PATH="$1"
REQUEST_FILE="$2"
# Resolve absolute git dir
GIT_DIR_REL="$(git -C "$CHECKOUT_PATH" rev-parse --git-dir)"
if [[ "$GIT_DIR_REL" == /* ]]; then
GIT_DIR="$GIT_DIR_REL"
else
GIT_DIR="$(cd "$CHECKOUT_PATH/$GIT_DIR_REL" && pwd)"
fi
LOCK_FILE="$GIT_DIR/codex-finalize.lock"
# Atomic lock acquisition via noclobber
if ! ( set -C; printf 'PID=%s\nSTARTED=%s\n' "$$" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$LOCK_FILE" ) 2>/dev/null; then
echo "ERROR: Lock file exists at $LOCK_FILE — another finalization may be running. Remove it manually after confirming no finalizer is running." >&2
exit 1
fi
trap 'rm -f "$LOCK_FILE"' EXIT
# Create run directory and announce start immediately
RUN_DIR="$(mktemp -d)"
echo "FINALIZE_STARTED $RUN_DIR"
# Stable pointer so callers can find the live log/result without parsing this
# process's own stdout (which may be buried in a background task transcript).
echo "$RUN_DIR" > "$GIT_DIR/codex-finalize-current-run"
# Record HEAD before Codex runs
HEAD_BEFORE="$(git -C "$CHECKOUT_PATH" rev-parse HEAD)"
# Invoke Codex, capturing exit code without relying on set -e
set +e
printf '%s\n\nREQUEST_FILE: %s\n' '$finalize-commit' "$REQUEST_FILE" \
| codex exec \
-C "$CHECKOUT_PATH" \
-s workspace-write \
-c sandbox_workspace_write.network_access=true \
--add-dir "$GIT_DIR" \
--ephemeral \
--output-schema "$SKILL_DIR/response-schema.json" \
-o "$RUN_DIR/result.json" \
- \
2>"$RUN_DIR/codex.log"
CODEX_EXIT=$?
set -e
# Record HEAD after Codex runs
HEAD_AFTER="$(git -C "$CHECKOUT_PATH" rev-parse HEAD)"
# Validate result; synthesize if missing, invalid, or Codex exited non-zero
if [ "$CODEX_EXIT" -ne 0 ] || [ ! -f "$RUN_DIR/result.json" ] || ! jq empty "$RUN_DIR/result.json" 2>/dev/null; then
if [ "$HEAD_AFTER" != "$HEAD_BEFORE" ]; then
printf '{"status":"inspect","commit_sha":"%s","error":"codex exited abnormally but HEAD advanced — commit may have succeeded"}\n' \
"$HEAD_AFTER" > "$RUN_DIR/result.json"
else
printf '{"status":"failed","error":"codex exited without valid output"}\n' \
> "$RUN_DIR/result.json"
fi
fi
echo "FINALIZE_DONE $RUN_DIR"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment