Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@
"command": "\"$CLAUDE_PROJECT_DIR\"/hooks/tdd-pretool-check.sh"
}
]
},
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/hooks/codex-gate-check.sh"
}
]
}
],
"InstructionsLoaded": [
Expand Down
1 change: 1 addition & 0 deletions cli/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const FILES = [
{ src: 'hooks/model-effort-check.sh', dest: '.claude/hooks/model-effort-check.sh', executable: true, base: REPO_ROOT },
{ src: 'hooks/precompact-seam-check.sh', dest: '.claude/hooks/precompact-seam-check.sh', executable: true, base: REPO_ROOT },
{ src: 'hooks/goal-confidence-check.sh', dest: '.claude/hooks/goal-confidence-check.sh', executable: true, base: REPO_ROOT },
{ src: 'hooks/codex-gate-check.sh', dest: '.claude/hooks/codex-gate-check.sh', executable: true, base: REPO_ROOT },
// #254 Bug 1: shared helper sourced by all hooks above. Must ship — without
// it, hooks emit "_find-sdlc-root.sh: No such file or directory" + the
// SDLC root walk-up logic is silently dead.
Expand Down
9 changes: 9 additions & 0 deletions cli/templates/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/tdd-pretool-check.sh"
}
]
},
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/codex-gate-check.sh"
}
]
}
],
"InstructionsLoaded": [
Expand Down
39 changes: 39 additions & 0 deletions hooks/codex-gate-check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/bin/bash
# PreToolUse hook — blocks git commit without cross-model review artifact
# Fires on Bash tool; only acts when the command contains "git commit"

set -e

# Skip gate if explicitly overridden (emergency bypass with logged justification)
[ "${CODEX_GATE_SKIP:-}" = "1" ] && exit 0

TOOL_INPUT=$(cat)

COMMAND=$(printf '%s' "$TOOL_INPUT" \
| grep -o '"command"[[:space:]]*:[[:space:]]*"[^"]*"' \
| head -1 \
| sed 's/"command"[[:space:]]*:[[:space:]]*"//; s/"$//')

case "$COMMAND" in
*"git commit"*) ;;
*) exit 0 ;;
esac

REVIEW_FILE=".reviews/handoff.json"

if [ ! -f "$REVIEW_FILE" ]; then
echo "CROSS-MODEL REVIEW REQUIRED: No .reviews/handoff.json found. Run Codex cross-model review before committing. Set CODEX_GATE_SKIP=1 to bypass with justification."
exit 0
fi

STATUS=$(grep -o '"status"[[:space:]]*:[[:space:]]*"[^"]*"' "$REVIEW_FILE" \
| head -1 \
| sed 's/.*"status"[[:space:]]*:[[:space:]]*"//; s/"$//')

case "$STATUS" in
CERTIFIED|REVIEWED) exit 0 ;;
*)
echo "CROSS-MODEL REVIEW REQUIRED: .reviews/handoff.json status is '$STATUS' (need REVIEWED or CERTIFIED). Run Codex cross-model review before committing. Set CODEX_GATE_SKIP=1 to bypass with justification."
exit 0
;;
esac
97 changes: 97 additions & 0 deletions tests/test-hooks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3541,6 +3541,103 @@ test_goal_confidence_check_silent_when_confidence_present
test_goal_confidence_check_dlc_binding_warning
test_goal_confidence_check_silent_on_status_and_clear

# ---- codex-gate-check.sh tests ----
echo ""
echo "--- codex-gate-check.sh ---"

test_codex_gate_blocks_commit_without_review() {
local tmpdir
tmpdir=$(mktemp -d)
mkdir -p "$tmpdir"
# No .reviews/handoff.json — should block
local out
out=$(printf '%s' '{"tool_input":{"command":"git commit -m \"fix: something\""}}' | (cd "$tmpdir" && "$HOOKS_DIR/codex-gate-check.sh") 2>&1) || true
rm -rf "$tmpdir"
if echo "$out" | grep -qi "cross-model review"; then
pass "codex gate blocks commit without review artifact"
else
fail "codex gate should block commit without review artifact (got: $out)"
fi
}

test_codex_gate_allows_commit_with_certified_review() {
local tmpdir
tmpdir=$(mktemp -d)
mkdir -p "$tmpdir/.reviews"
printf '{"status":"CERTIFIED","score":9}' > "$tmpdir/.reviews/handoff.json"
local out
out=$(printf '%s' '{"tool_input":{"command":"git commit -m \"fix: something\""}}' | (cd "$tmpdir" && "$HOOKS_DIR/codex-gate-check.sh") 2>&1) || true
rm -rf "$tmpdir"
if [ -z "$out" ]; then
pass "codex gate allows commit with CERTIFIED review"
else
fail "codex gate should be silent with CERTIFIED review (got: $out)"
fi
}

test_codex_gate_allows_commit_with_reviewed_status() {
local tmpdir
tmpdir=$(mktemp -d)
mkdir -p "$tmpdir/.reviews"
printf '{"status":"REVIEWED"}' > "$tmpdir/.reviews/handoff.json"
local out
out=$(printf '%s' '{"tool_input":{"command":"git commit -m \"fix: something\""}}' | (cd "$tmpdir" && "$HOOKS_DIR/codex-gate-check.sh") 2>&1) || true
rm -rf "$tmpdir"
if [ -z "$out" ]; then
pass "codex gate allows commit with REVIEWED status"
else
fail "codex gate should be silent with REVIEWED review (got: $out)"
fi
}

test_codex_gate_silent_on_non_commit_commands() {
local tmpdir
tmpdir=$(mktemp -d)
local out
out=$(printf '%s' '{"tool_input":{"command":"git status"}}' | (cd "$tmpdir" && "$HOOKS_DIR/codex-gate-check.sh") 2>&1) || true
rm -rf "$tmpdir"
if [ -z "$out" ]; then
pass "codex gate silent on non-commit commands"
else
fail "codex gate should be silent on git status (got: $out)"
fi
}

test_codex_gate_blocks_on_invalid_status() {
local tmpdir
tmpdir=$(mktemp -d)
mkdir -p "$tmpdir/.reviews"
printf '{"status":"PENDING"}' > "$tmpdir/.reviews/handoff.json"
local out
out=$(printf '%s' '{"tool_input":{"command":"git commit -m \"fix: thing\""}}' | (cd "$tmpdir" && "$HOOKS_DIR/codex-gate-check.sh") 2>&1) || true
rm -rf "$tmpdir"
if echo "$out" | grep -qi "cross-model review"; then
pass "codex gate blocks commit with PENDING status"
else
fail "codex gate should block with non-certified status (got: $out)"
fi
}

test_codex_gate_skip_override() {
local tmpdir
tmpdir=$(mktemp -d)
local out
out=$(printf '%s' '{"tool_input":{"command":"git commit -m \"fix: something\""}}' | (cd "$tmpdir" && CODEX_GATE_SKIP=1 "$HOOKS_DIR/codex-gate-check.sh") 2>&1) || true
rm -rf "$tmpdir"
if [ -z "$out" ]; then
pass "codex gate respects CODEX_GATE_SKIP=1 override"
else
fail "codex gate should be silent with CODEX_GATE_SKIP=1 (got: $out)"
fi
}

test_codex_gate_blocks_commit_without_review
test_codex_gate_allows_commit_with_certified_review
test_codex_gate_allows_commit_with_reviewed_status
test_codex_gate_silent_on_non_commit_commands
test_codex_gate_blocks_on_invalid_status
test_codex_gate_skip_override

echo ""
echo "=== Results ==="
echo "Passed: $PASSED"
Expand Down