Code Review Hooks

Overview

CodeGraph provides automated code review through two mechanisms:

  1. Claude Code hooks — standalone Python scripts in .claude/hooks/ that run automatically at lifecycle points (SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop). They communicate via JSON stdin/stdout.
  2. CLI review pipelinepython -m src.cli review command with its own aggregator, scope-aware filtering, and SARIF output.

Both share the review_pipeline configuration and parse scope awareness, but implement filtering differently.

Hooks

Hook Lifecycle Timeout Purpose
session_context.py SessionStart 10s Detect project, check CPG freshness, load parse scope
enrich_prompt.py UserPromptSubmit 15s Inject CPG context into prompts (max 3 entities)
pre_tool_use.py PreToolUse 8s Complexity gate (CC > 15), fan-out check, registration completeness
commit_analysis.py PostToolUse (Bash) 60s Quality analysis after git commit with CPG freshness update
post_analysis.py Stop 10s Dead code, missing tests, complexity spikes, interface gaps
cli_error_monitor.py PostToolUse (Bash) 5s Detect 6 CLI error patterns

cli_error_monitor Patterns

The monitor detects these specific error patterns in Bash output:

  1. Database not found
  2. DatabaseNotConfiguredError
  3. DuckDB __spec__ import bug
  4. ImportError / ModuleNotFoundError
  5. Python traceback (generic)
  6. RuntimeWarning (module import)

Timeout Budgets

commit_analysis.py has an internal 58s budget (2s margin from 60s timeout):

Phase Budget Description
CPG freshness check 2s Compare HEAD commit with DB
CPG update (if stale) 40s Re-parse via gocpg
Quality analysis 16s CC, TODO/FIXME, blast radius

Thresholds

Hook Threshold Value
pre_tool_use.py Cyclomatic Complexity 15
pre_tool_use.py Fan-Out 30
commit_analysis.py Cyclomatic Complexity 10
commit_analysis.py Fan-Out 30
commit_analysis.py Max files 20
post_analysis.py CC spike ratio 2.0x
post_analysis.py Max files 5
enrich_prompt.py Max entities 3

Architecture

stdin (JSON)  Hook Script  stdout (JSON)
                   
              _utils.py      (shared helpers: gocpg queries, scope loading)
              _feedback.py   (ReviewFeedback/ReviewFinding dataclasses)
              _metrics.py    (JSONL performance logging)
              _session_cache.py  (file-based project context cache, TTL=3600s)
              _project_detector.py  (auto-detect project by CWD/source_path matching)

Support Modules

Module Key exports
_utils.py read_stdin_json(), safe_json_output(), run_gocpg_query(), extract_entities(), load_parse_scope(), is_partial_scope(), tests_in_scope()
_feedback.py ReviewFinding (8 fields), ReviewFeedback (9 fields), FINDING_TYPES, SEVERITY_ORDER
_metrics.py timed_hook() context manager, log_hook_metric()
_session_cache.py get_project_with_cache(), set_cached_project(), invalidate_cache() — TTL 3600s, stored in .claude/.cache/session_project.json
_project_detector.py detect_project(cwd) — matches CWD against projects.registry[*].source_path, supports ${VAR:-default} env vars

Structured Feedback Protocol

All hooks output JSON with optional additionalContext (markdown rendered to user):

{
  "additionalContext": "## Review Report\n..."
}

The ReviewFeedback class in _feedback.py generates structured markdown with: - Severity-sorted findings table - Scope disclaimer (when parse scope is partial) - Suppressed count (findings filtered due to scope) - Duration and metadata

Finding Types

Six finding types are defined in FINDING_TYPES:

Type Description Used by
complexity High cyclomatic complexity or fan-out pre_tool_use, commit_analysis, post_analysis
dead_code Unreachable or uncalled code commit_analysis
missing_test No test file for source module post_analysis
blast_radius Many callers affected by change commit_analysis
security Security vulnerability CLI review pipeline
interface_gap Public functions not exposed via any interface pre_tool_use, post_analysis

Parse Scope Awareness

When a CPG is built with excluded directories (e.g., only backend without tests/frontend), scope-aware filtering is applied. The behavior differs between hooks and the CLI review pipeline:

Hooks Behavior

Finding Type Condition Hook Action
dead_code Scope is partial commit_analysis.py Add disclaimer block to report
blast_radius Scope is partial commit_analysis.py Add disclaimer block to report
missing_test include_tests=false post_analysis.py Suppress entirely, increment suppressed_count
complexity all Not affected (per-method metric)
security all Not affected (real vulnerability)
interface_gap all Not affected

CLI Review Pipeline Behavior

The CLI review command uses ReviewAggregator (src/review/aggregator.py) with different logic:

Finding Type Condition Action
dead_code Scope is partial + demote_dead_code_partial_scope=true Demote to info, set scope_limited=True
blast_radius Scope is partial + demote_dead_code_partial_scope=true Demote to info, set scope_limited=True
missing_test suppress_tests_outside_scope=true + tests not in scope Suppress (return None)

A disclaimer block is added to the report when scope is limited.

CLI Review Command

# Review changes against a base ref
python -m src.cli review --base-ref HEAD~3

# Review with specific database
python -m src.cli review --db data/projects/myproject.duckdb --base-ref HEAD~3

# Review staged changes
python -m src.cli review --staged

# Review specific files
python -m src.cli review --files src/api/main.py src/auth.py

# Output formats
python -m src.cli review --format json --output-file report.json
python -m src.cli review --format sarif --output-file report.sarif
python -m src.cli review --base-ref HEAD~5 --sarif-file out.sarif

# Skip security analysis
python -m src.cli review --no-security
Argument Description
--db PATH Path to DuckDB CPG database (default: active project)
--base-ref REF Git base ref for diff review (e.g. HEAD~3, origin/main)
--staged Review staged changes only
--files FILE... Review specific files
--no-security Skip security analysis
--format {markdown,json,sarif} Output format (default: markdown)
--output-file PATH Write output to file
--sarif-file PATH Write SARIF output separately

Exit codes: 0 = clean or medium/low only, 1 = critical or high findings.

Metrics & Monitoring

Hooks log execution metrics to data/hook_metrics.jsonl:

{"timestamp": "2026-03-06T12:00:00Z", "hook": "commit_analysis", "duration_ms": 1234.5, "findings": 3, "project": "codegraph", "status": "ok"}

View statistics:

python -m src.cli dogfood hooks-status
python -m src.cli dogfood hooks-status --last 50
python -m src.cli dogfood hooks-status --hook commit_analysis

Graceful Degradation

All hooks follow fail-open design: - Missing gocpg binary: skip CPG queries, output empty context - Corrupt/missing DB: skip analysis, warn in metrics - Timeout: partial results returned within budget - Import errors: fallback to subprocess path (commit_analysis uses DuckDB direct → gocpg subprocess) - Any unhandled exception: caught at top level, empty JSON output

Configuration

# config.yaml
review_pipeline:
  # Project detection
  auto_detect_project: true             # Auto-detect project by CWD
  detect_by_source_path: true           # Match CWD against projects.registry[*].source_path
  # CPG freshness
  auto_reparse: prompt                  # Behavior on stale CPG: prompt | auto | off
  freshness_check_on_session: true      # Check CPG freshness at SessionStart
  max_reparse_timeout_seconds: 60       # Timeout for CPG re-parse
  stale_threshold_commits: 0            # Commits before CPG is stale (0 = any new commit)
  # Output
  max_findings_in_summary: 10           # Max findings shown in summary
  # Parse scope awareness
  scope_aware_filtering: true           # Enable scope-aware filtering
  suppress_tests_outside_scope: true    # Suppress missing_test when tests not in scope
  demote_dead_code_partial_scope: true  # Demote dead_code/blast_radius in CLI review
  show_scope_disclaimer: true           # Show disclaimer when scope is partial
  # Metrics
  metrics_enabled: true                 # Enable JSONL metrics logging
  metrics_file: data/hook_metrics.jsonl # Metrics output file
  fail_open: true                       # Continue on errors (fail-open)

# Dogfooding (used by commit_analysis.py)
dogfooding:
  enabled: true
  auto_update_cpg: true
  cpg_update_timeout: 40                # Timeout for CPG update in commit_analysis
  analysis_timeout: 16                  # Timeout for quality analysis phase

Adding a New Hook

  1. Create a Python script in .claude/hooks/
  2. Read JSON from stdin with read_stdin_json() from _utils.py
  3. Wrap logic in with timed_hook("hook_name") from _metrics.py
  4. Output JSON to stdout with safe_json_output() from _utils.py
  5. Register in .claude/settings.json

settings.json Format

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "python .claude/hooks/my_hook.py",
            "timeout": 8000
          }
        ]
      }
    ]
  }
}
Field Description
Top-level key Lifecycle event: SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop
matcher Tool name filter (e.g. "Bash"). Empty string = all tools
type Always "command"
command Full path to Python script
timeout Timeout in milliseconds

Directory: .claude/hooks/ Last updated: March 2026