Git Hooks Powered by Codex CLI: Pre-Commit Review, Commit Message Generation, and Pre-Push Validation
Git Hooks Powered by Codex CLI: Pre-Commit Review, Commit Message Generation, and Pre-Push Validation
Git hooks are the last line of defence before code leaves your machine. Most teams wire them up to linters, formatters, and type-checkers — fast, deterministic tools that catch surface-level issues. But what about logic errors, security anti-patterns, or commit messages that say “fix stuff”? This is where codex exec transforms Git hooks from syntax police into semantic reviewers.
This article walks through three concrete Git hook implementations powered by Codex CLI’s non-interactive mode: a pre-commit review gate, a commit-msg generator that enforces Conventional Commits, and a pre-push validation sweep. Each hook ships as a working script you can drop into any repository today.
Why Use Codex CLI Inside Git Hooks?
Traditional linters catch what violates a rule. An LLM-powered hook catches what looks wrong to a senior reviewer — the kind of feedback that usually only surfaces in pull request review, hours or days later 1. Running codex exec inside a hook pulls that feedback forward to the moment of commit.
Key advantages:
- Semantic analysis — catches logic bugs, SQL injection, hardcoded secrets, and architectural violations that no linter rule covers 2
- Structured output — the
--output-schemaflag returns machine-parseable JSON, so your hook can make binary pass/fail decisions programmatically 3 - Read-only safety —
--sandbox read-onlyensures the hook never modifies your working tree 4 - Speed control — use
-c model=codex-sparkfor sub-3-second hooks on small diffs, or-c reasoning_effort=lowto keep latency tight 5
flowchart LR
A[git commit] --> B{pre-commit hook}
B -->|staged diff| C[codex exec\n--sandbox read-only]
C -->|structured JSON| D{P0 findings?}
D -->|yes| E[❌ Block commit]
D -->|no| F{commit-msg hook}
F -->|diff + message| G[codex exec\n--output-schema]
G --> H[Write Conventional\nCommit message]
H --> I[✅ Commit created]
I --> J{git push}
J --> K{pre-push hook}
K -->|full branch diff| L[codex exec\ndeep review]
L -->|pass| M[✅ Push proceeds]
L -->|fail| N[❌ Block push]
Prerequisites
You need Codex CLI v0.122.0 or later (for --ignore-user-config isolation) and a valid CODEX_API_KEY or ChatGPT session 6. All examples use Bash and assume a Unix-like environment; Windows users should run these under WSL2.
# Verify codex is available
codex --version
# Should print 0.125.x or later
Hook 1: Pre-Commit Semantic Review
This hook pipes the staged diff into codex exec, asks it to review for critical issues only, and blocks the commit if any P0 findings are returned.
The JSON Schema
Create .codex/schemas/pre-commit-review.json:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"findings": {
"type": "array",
"items": {
"type": "object",
"properties": {
"priority": {
"type": "integer",
"minimum": 0,
"maximum": 3,
"description": "0=critical, 1=important, 2=suggestion, 3=nit"
},
"title": { "type": "string" },
"file": { "type": "string" },
"line": { "type": "integer" },
"body": { "type": "string" }
},
"required": ["priority", "title", "file", "body"]
}
},
"summary": { "type": "string" }
},
"required": ["findings", "summary"]
}
The Hook Script
Create .git/hooks/pre-commit (or use Lefthook — covered below):
#!/usr/bin/env bash
set -euo pipefail
DIFF=$(git diff --cached --diff-algorithm=minimal)
# Skip if nothing staged or diff is trivial
if [ -z "$DIFF" ] || [ "$(echo "$DIFF" | wc -l)" -lt 5 ]; then
exit 0
fi
SCHEMA="$(git rev-parse --show-toplevel)/.codex/schemas/pre-commit-review.json"
# Fall back gracefully if codex is not installed
if ! command -v codex &>/dev/null; then
echo "⚠️ codex not found — skipping AI review"
exit 0
fi
RESULT=$(echo "$DIFF" | codex exec \
--sandbox read-only \
--ephemeral \
-c model=codex-spark \
-c reasoning_effort=low \
--output-schema "$SCHEMA" \
-o /dev/stdout \
"Review this staged diff. Report ONLY P0 (security, data loss, crash) and P1 (correctness) issues. Ignore style, naming, and formatting." \
2>/dev/null) || {
echo "⚠️ codex exec failed — allowing commit"
exit 0
}
# Parse findings with jq
P0_COUNT=$(echo "$RESULT" | jq '[.findings[] | select(.priority == 0)] | length')
if [ "$P0_COUNT" -gt 0 ]; then
echo "❌ Codex found $P0_COUNT critical issue(s):"
echo "$RESULT" | jq -r '.findings[] | select(.priority == 0) | " \(.file): \(.title) — \(.body)"'
exit 1
fi
P1_COUNT=$(echo "$RESULT" | jq '[.findings[] | select(.priority == 1)] | length')
if [ "$P1_COUNT" -gt 0 ]; then
echo "⚠️ Codex flagged $P1_COUNT important issue(s) (non-blocking):"
echo "$RESULT" | jq -r '.findings[] | select(.priority == 1) | " \(.file): \(.title)"'
fi
exit 0
Make it executable: chmod +x .git/hooks/pre-commit.
Key Design Decisions
| Decision | Rationale |
|---|---|
codex-spark model |
Sub-2-second latency for typical diffs; adequate for pattern-matching security issues 5 |
--ephemeral |
No session files persisted — hooks should be stateless 3 |
--sandbox read-only |
Prevents the agent from modifying files during review 4 |
| Graceful fallback on failure | A broken hook should never block a developer’s workflow |
| P0-only blocking | Reduces false positive friction; P1 issues are informational |
Hook 2: Commit Message Generation
A commit-msg hook that rewrites a placeholder message into a Conventional Commits 7 format, using the staged diff as context.
Create .git/hooks/commit-msg:
#!/usr/bin/env bash
set -euo pipefail
MSG_FILE="$1"
CURRENT_MSG=$(cat "$MSG_FILE")
# Skip if message already looks like a conventional commit
if echo "$CURRENT_MSG" | grep -qE '^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?!?:'; then
exit 0
fi
# Skip merge commits and fixup commits
if echo "$CURRENT_MSG" | grep -qE '^(Merge|fixup!|squash!|amend!)'; then
exit 0
fi
if ! command -v codex &>/dev/null; then
exit 0
fi
DIFF=$(git diff --cached --diff-algorithm=minimal)
GENERATED=$(echo "$DIFF" | codex exec \
--sandbox read-only \
--ephemeral \
-c model=codex-spark \
-c reasoning_effort=low \
"Generate a Conventional Commits message for this diff.
Format: <type>(<scope>): <description>
<optional body>
Rules:
- type: feat|fix|docs|refactor|test|perf|build|ci|chore
- scope: the primary module or directory affected
- description: imperative mood, lowercase, no period, max 72 chars
- body: wrap at 72 chars, explain WHY not WHAT
- If the original message '$CURRENT_MSG' contains useful context, incorporate it
- Output ONLY the commit message, nothing else" \
2>/dev/null) || {
exit 0
}
# Only overwrite if we got a non-empty result
if [ -n "$GENERATED" ] && [ "$(echo "$GENERATED" | wc -l)" -le 20 ]; then
echo "$GENERATED" > "$MSG_FILE"
fi
exit 0
This hook is non-blocking — if codex exec fails or returns empty, the original message is preserved.
Hook 3: Pre-Push Validation
The pre-push hook runs a deeper review across all commits being pushed, using the full-capability model. Because this runs less frequently and latency is more acceptable, we can afford a thorough analysis.
Create .git/hooks/pre-push:
#!/usr/bin/env bash
set -euo pipefail
REMOTE="$1"
# Read refs from stdin (git sends them)
while read -r LOCAL_REF LOCAL_SHA REMOTE_REF REMOTE_SHA; do
# Skip deletions
if [ "$LOCAL_SHA" = "0000000000000000000000000000000000000000" ]; then
continue
fi
# Determine diff range
if [ "$REMOTE_SHA" = "0000000000000000000000000000000000000000" ]; then
# New branch — diff against main
RANGE="origin/main...$LOCAL_SHA"
else
RANGE="$REMOTE_SHA...$LOCAL_SHA"
fi
DIFF=$(git diff "$RANGE" --diff-algorithm=minimal 2>/dev/null || true)
if [ -z "$DIFF" ] || [ "$(echo "$DIFF" | wc -l)" -lt 10 ]; then
continue
fi
if ! command -v codex &>/dev/null; then
exit 0
fi
SCHEMA="$(git rev-parse --show-toplevel)/.codex/schemas/pre-commit-review.json"
echo "🔍 Running Codex pre-push review on $(echo "$DIFF" | wc -l) lines..."
RESULT=$(echo "$DIFF" | codex exec \
--sandbox read-only \
--ephemeral \
-c reasoning_effort=medium \
--output-schema "$SCHEMA" \
-o /dev/stdout \
"Deep review of all changes being pushed. Check for:
1. Security vulnerabilities (injection, auth bypass, secret exposure)
2. Data integrity risks (race conditions, missing transactions, unsafe deletes)
3. Breaking API changes without version bumps
4. Missing error handling on network/IO operations
Report only P0 and P1 issues." \
2>/dev/null) || {
echo "⚠️ Codex review failed — allowing push"
exit 0
}
P0_COUNT=$(echo "$RESULT" | jq '[.findings[] | select(.priority == 0)] | length')
if [ "$P0_COUNT" -gt 0 ]; then
echo "❌ Codex blocked push — $P0_COUNT critical issue(s):"
echo "$RESULT" | jq -r '.findings[] | select(.priority == 0) | " \(.file):\(.line // "?") \(.title)\n \(.body)\n"'
echo "Fix these issues or use 'git push --no-verify' to bypass."
exit 1
fi
done
exit 0
Integration with Lefthook
Raw .git/hooks/ scripts are not version-controlled by default. Lefthook 8 solves this with a single lefthook.yml that lives in your repository:
# lefthook.yml
pre-commit:
commands:
codex-review:
glob: "*.{ts,tsx,js,jsx,py,go,rs,rb,java,kt}"
run: |
git diff --cached --diff-algorithm=minimal | codex exec \
--sandbox read-only --ephemeral \
-c model=codex-spark -c reasoning_effort=low \
--output-schema .codex/schemas/pre-commit-review.json \
-o /dev/stdout \
"Review staged diff. Report P0 issues only." 2>/dev/null \
| jq -e '[.findings[] | select(.priority == 0)] | length == 0'
lint:
glob: "*.{ts,tsx}"
run: npx eslint --max-warnings=0 {staged_files}
format:
glob: "*.{ts,tsx,json,md}"
run: npx prettier --check {staged_files}
commit-msg:
commands:
conventional:
run: |
if ! grep -qE '^(feat|fix|docs|refactor|test|perf|build|ci|chore)' "$1"; then
git diff --cached | codex exec --sandbox read-only --ephemeral \
-c model=codex-spark "Generate conventional commit msg for this diff" \
2>/dev/null > "$1" || true
fi
pre-push:
commands:
codex-deep-review:
run: |
DIFF=$(git diff origin/main...HEAD)
[ -z "$DIFF" ] && exit 0
echo "$DIFF" | codex exec --sandbox read-only --ephemeral \
-c reasoning_effort=medium \
--output-schema .codex/schemas/pre-commit-review.json \
-o /dev/stdout \
"Deep security and correctness review." 2>/dev/null \
| jq -e '[.findings[] | select(.priority == 0)] | length == 0'
Install with:
npm install --save-dev lefthook
npx lefthook install
Lefthook runs hooks in parallel, respects glob filters so only relevant files trigger the Codex review, and the configuration is committed alongside your code 8.
Preventing Agents from Bypassing Hooks
If you use Codex CLI itself for development, the agent might attempt git commit --no-verify to skip slow hooks. Block this with a Codex internal hook (distinct from Git hooks):
# .codex/config.toml
[features]
codex_hooks = true
[[hooks.PreToolUse]]
matcher = "^Bash$"
[[hooks.PreToolUse.hooks]]
type = "command"
command = 'node .codex/hooks/block-no-verify.mjs'
timeout = 5
statusMessage = "Checking for --no-verify bypass"
The blocking script (from Steve Kinney’s self-testing agents course 9):
// .codex/hooks/block-no-verify.mjs
import fs from 'node:fs';
const input = JSON.parse(fs.readFileSync(0, 'utf8'));
const command = input.tool_input?.command ?? '';
if (/(^|\s)--no-verify(\s|$)/.test(command)) {
process.stdout.write(JSON.stringify({
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'deny',
permissionDecisionReason:
'Do not use --no-verify. Fix the failing hook instead.',
},
}));
}
process.exit(0);
Add the corresponding rule to your AGENTS.md:
## Git Workflow Rules
- Never use `--no-verify` when committing or pushing
- If a Git hook fails, fix the underlying issue rather than bypassing the hook
- All commit messages must follow Conventional Commits format
Performance Tuning
Hook latency is the primary adoption barrier. Here is a decision matrix:
| Hook | Target Latency | Model | Reasoning Effort | Notes |
|---|---|---|---|---|
| pre-commit | < 3s | codex-spark |
low |
Only P0 findings; small staged diffs |
| commit-msg | < 2s | codex-spark |
low |
Single-shot generation, no schema |
| pre-push | < 15s | gpt-5.4 or default |
medium |
Full branch diff; deeper analysis acceptable |
For monorepos with large diffs, truncate the input:
# Cap diff at 8000 lines to stay within token budget
DIFF=$(git diff --cached --diff-algorithm=minimal | head -8000)
Prompt caching 10 helps when the same AGENTS.md and schema are used repeatedly — the stable prefix (system instructions + schema) gets cached server-side, reducing latency by up to 80% on subsequent runs.
Cost Considerations
At current pricing (April 2026), codex-spark input tokens cost approximately $0.15 per million 5. A typical staged diff of 200 lines (~2,000 tokens input) costs roughly $0.0003 per commit — negligible even at high commit frequency. The pre-push hook with gpt-5.4 costs more but runs far less frequently.
⚠️ Exact pricing depends on your plan tier and whether you are using API credits or a ChatGPT subscription. Check the Codex pricing page for current rates.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Hook hangs indefinitely | Network timeout or auth failure | Add timeout 30 wrapper; ensure CODEX_API_KEY is set |
codex: command not found |
Not on $PATH in hook context |
Use full path: $(npm root -g)/@openai/codex/bin/codex |
| Empty JSON output | Diff too small for schema enforcement | Add minimum line-count guard before calling codex |
| False positives blocking commits | Model hallucinating issues | Restrict to P0 only; add --ephemeral to prevent context leakage |
| Slow pre-commit (>5s) | Large diff or wrong model | Switch to codex-spark; truncate diff; check prompt caching |
Citations
-
OpenAI, “Best practices — Codex,” https://developers.openai.com/codex/learn/best-practices ↩
-
OpenAI, “Codex CLI Automatic Code Review: PR Integration and Pre-Commit Workflows,” https://codex.danielvaughan.com/2026/03/27/codex-cli-code-review-pr-integration/ ↩
-
OpenAI, “Non-interactive mode — Codex,” https://developers.openai.com/codex/noninteractive ↩ ↩2
-
OpenAI, “Command line options — Codex CLI,” https://developers.openai.com/codex/cli/reference ↩ ↩2
-
OpenAI, “Codex CLI Speed Stack article — codex-spark model and pricing,” https://developers.openai.com/codex/changelog ↩ ↩2 ↩3
-
OpenAI, “Codex CLI — Getting Started,” https://developers.openai.com/codex/cli ↩
-
Conventional Commits, “Specification v1.0.0,” https://www.conventionalcommits.org/en/v1.0.0/ ↩
-
Lefthook, “Git hooks manager,” https://github.com/evilmartians/lefthook; Steve Kinney, “Git Hooks with Lefthook,” https://stevekinney.com/courses/self-testing-ai-agents/git-hooks-with-lefthook ↩ ↩2
-
Steve Kinney, “Self-Testing AI Agents — Blocking –no-verify,” https://stevekinney.com/courses/self-testing-ai-agents/git-hooks-with-lefthook ↩
-
OpenAI, “Prompt Caching 201,” https://developers.openai.com/cookbook/examples/prompt_caching_201 ↩