C++ quality gate for Cursor: 3 automated checks before every commit (sanitizers + cppcheck + target build)

The problem

You write C++ in Cursor, it compiles fine locally, you commit and push — then:

  • The CI server (or grader) uses an older compiler and rejects something you used
    freely.
  • A subtle heap-use-after-free slips through because you never ran under
    AddressSanitizer.
  • cppcheck would have caught an obvious style issue, but you only run it
    occasionally.

This guide shows how to set up a 3-gate local quality check that runs
automatically before commits, catches all three classes of problem, and integrates
cleanly with Cursor’s agent workflow.


The 3 gates

Gate What it checks Blocks?
1 — Target build Compiles with your deployment flags (older standard, strict warnings) Yes
2 — Sanitizer build Memory errors (ASan) + undefined behavior (UBSan) Yes
3 — Static analysis cppcheck: style, performance, portability, suspicious constructs Report; ask

Gate 1 is the key insight: compile locally with the same flags your target
environment uses
, not just whatever your IDE defaults to. If you develop on GCC
15 / C++23 but your CI or grader uses GCC 9 / C++17, Gate 1 catches the mismatch
immediately on your machine instead of at submission time.


Drop-in Makefile targets

Save this as quality.mk in your project root and include it from your main
Makefile (include quality.mk), or copy the targets you need directly.

Note: Makefile recipe lines (the commands under each target) must use a
tab character, not spaces. Most editors preserve this when you paste from a
code block, but double-check if make reports “missing separator” errors.

# quality.mk — 3-gate C++ quality check
# Adjust TARGET_CXX_STD to match your deployment environment.
# Common values: gnu++1z (GCC 9 C++17), c++17, c++20, c++23

CXX               ?= g++
TARGET_CXX_STD    ?= gnu++1z          # change to match your target
CXXFLAGS_TARGET   = -std=$(TARGET_CXX_STD) -Wall -Wextra -fPIC -c
CXXFLAGS_SANITIZE = -std=$(TARGET_CXX_STD) -g -fsanitize=address,undefined -fno-omit-frame-pointer -Wall -Wextra
CPPCHECK          ?= cppcheck
CPPCHECK_FLAGS    = --enable=warning,style,performance --std=c++17 --error-exitcode=1 --quiet

SOURCES ?= $(wildcard *.cpp)
HEADERS ?= $(wildcard *.hh *.h)
TARGET  ?= app

.PHONY: check sanitize cppcheck quality test-sanitize clean-sanitize

check:
	@echo "=== Gate 1: target build (-std=$(TARGET_CXX_STD)) ==="
	$(CXX) $(CXXFLAGS_TARGET) $(SOURCES)
	@echo "PASS"; rm -f *.o

sanitize:
	@echo "=== Gate 2: ASan + UBSan ==="
	$(CXX) $(CXXFLAGS_SANITIZE) $(SOURCES) -o $(TARGET)_sanitized
	@echo "PASS"

cppcheck:
	@echo "=== Gate 3: cppcheck ==="
	$(CPPCHECK) $(CPPCHECK_FLAGS) $(SOURCES) $(HEADERS)
	@echo "PASS"

quality: check sanitize cppcheck
	@echo "=== All 3 gates passed ==="

test-sanitize: sanitize
	@echo "=== Running sanitizer-instrumented binary ==="
	./$(TARGET)_sanitized

clean-sanitize:
	rm -f $(TARGET)_sanitized *.o

Run all three:

make quality

Or individually:

make check       # Gate 1: does it compile for the target?
make sanitize    # Gate 2: any memory/UB issues?
make cppcheck    # Gate 3: any style/performance findings?

Windows / MinGW note (Gate 2)

On Windows with MSYS2 ucrt64, g++ -fsanitize=address,undefined
produces a linker error (collect2: ld returned 1 exit status). This is
a platform limitation — the sanitizer runtime is not bundled with
MSYS2 ucrt64 GCC by default. It is not a bug in your code.

Workaround options:

  1. WSL2 — run Gate 2 in a Linux layer (recommended; full sanitizer output).
  2. Dockerdocker run --rm -v .:/src gcc:12 bash -c "cd /src && make sanitize".
  3. CI — configure a Linux CI runner (GitHub Actions, GitLab CI) that runs
    Gate 2 on every push while Gate 1 and Gate 3 run locally on Windows.

Detect the linker-error pattern in a pre-push shell hook and downgrade Gate 2 to a
warning rather than a hard failure on Windows:

output=$(g++ -fsanitize=address,undefined *.cpp -o app_sanitized 2>&1)
rc=$?
if echo "$output" | grep -qE "(ld returned|collect2)"; then
    echo "WARN Gate 2: sanitizer linker error — platform limitation."
    echo "     Run on Linux/WSL2 for full sanitizer coverage."
elif [ $rc -ne 0 ]; then
    echo "FAIL Gate 2:"
    echo "$output"
    exit 1
fi

Pre-push hook (optional but recommended)

A pre-push Git hook runs the quality gate automatically before every git push, so
you never accidentally push code that fails Gate 1 or Gate 3.

Create .git/hooks/pre-push (or tools/hooks/pre-push if you symlink it):

#!/usr/bin/env bash
# Pre-push quality gate — runs on every push to any branch.
set -euo pipefail

BRANCH=$(git rev-parse --abbrev-ref HEAD)
echo "--- Pre-push quality gate ($BRANCH) ---"

# Gate 1: target build (always blocking)
echo "[Gate 1] Target build..."
if ! make check; then
    echo "FAIL Gate 1: fix build errors before pushing."
    exit 1
fi

# Gate 2: sanitizers (blocking on Linux; advisory on Windows)
echo "[Gate 2] Sanitizer build..."
san_out=$(make sanitize 2>&1) || true
if echo "$san_out" | grep -qE "(ld returned|collect2)"; then
    echo "WARN Gate 2: sanitizer linker error (platform limitation — run on Linux/WSL2)."
elif echo "$san_out" | grep -qi "error"; then
    echo "FAIL Gate 2: sanitizer build failed."
    echo "$san_out"
    exit 1
fi

# Gate 3: cppcheck (blocking on main; advisory on feature branches)
echo "[Gate 3] cppcheck..."
if ! make cppcheck; then
    if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then
        echo "FAIL Gate 3: fix cppcheck findings before pushing to $BRANCH."
        exit 1
    else
        echo "WARN Gate 3: cppcheck findings — fix before merging to main."
    fi
fi

echo "--- Quality gate passed ($BRANCH) ---"

Make it executable:

chmod +x .git/hooks/pre-push

This hook:

  • Always blocks on Gate 1 (a non-compiling push is never useful).
  • Detects the Windows sanitizer linker error and downgrades Gate 2 to a warning
    instead of a hard block.
  • Blocks Gate 3 only on main/master; feature branches get a warning so you can
    push WIP code for review without cppcheck being a blocker.

Integrating with Cursor agents

If you use Cursor’s agent workflow, you can automate the quality gate inside the
agent’s version-management skill. The agent runs all three checks before writing a
commit message — any gate failure causes the agent to report the finding and ask
how to proceed rather than silently committing broken code.

Concretely, in your agent skill or rule:

# Agent skill/rule — quality gate instruction
Before every C++ commit:
1. Run `make check`   — if it fails, stop and report errors.
2. Run `make sanitize` — if it fails (not a linker error), stop and report.
3. Run `make cppcheck` — report findings; ask user before committing if any.
Only proceed to `git commit` when all three are clean (or user explicitly
confirms skipping a gate with a reason).

The key behavior: the agent asks rather than skipping silently. This keeps the
human in the loop on any quality finding.


Quick reference

Command What it does
make check Gate 1 — compile with target flags, no output binary
make sanitize Gate 2 — ASan + UBSan instrumented binary
make cppcheck Gate 3 — static analysis report
make quality All 3 gates in sequence
make test-sanitize Gate 2 + run the binary (leak/UB report on exit)

Platform notes

Platform Gate 1 Gate 2 (sanitizers) Gate 3 (cppcheck)
Linux Works as-is Full support (ASan + UBSan link and run) Works as-is
macOS Works as-is Full support (Apple Clang supports ASan/UBSan) Works as-is
Windows (MSYS2) Works as-is Linker error — see § Windows / MinGW note Works as-is

macOS note: the default g++ on macOS is actually Apple Clang, not
GCC. Install the Xcode Command Line Tools first
(xcode-select --install) and cppcheck via Homebrew
(brew install cppcheck). The flags in this guide (-std=gnu++1z,
-fsanitize=..., -fPIC, -Wall -Wextra) all work with both
compilers. If your target environment uses GCC specifically and you need
to match its behavior exactly, install GCC via Homebrew
(brew install gcc) — it provides g++-14 or similar alongside the
system Clang.


Why this matters for C++ specifically

  • Sanitizers catch bugs that code review misses: use-after-free, integer
    overflow, signed overflow, null dereference — none of these show up in
    -Wall -Wextra output, but all of them produce immediate, actionable output with
    ASan/UBSan.
  • Target build catches API drift: C++20 deprecates things that C++17 compiles
    quietly; GCC 9 rejects some C++20 constructs that GCC 15 accepts. Catching this
    locally is infinitely cheaper than discovering it at deployment.
  • cppcheck complements the compiler: it finds logic errors, uninitialized
    variables, and style issues the compiler won’t flag even at -Wall -Wextra.

Happy to answer questions about platform-specific setup (Windows/macOS/Linux), CI
integration, or fitting this into an existing Makefile.

Updates to using sanitizers in Windows - Address (ASan) and Undefined Behavior (UBSan):

Windows / MinGW note (Gate 2)

On Windows with MSYS2 ucrt64, g++ -fsanitize=address,undefined fails at
link time (collect2: ld returned 1 exit status, often missing -lasan).
GNU ASan/UBSan runtimes are not built for MinGW-w64 GCC in that
environment — there is no pacman package that fixes it. This matches the
MSYS2 environment matrix (Sanitizers: Yes only for CLANG64). It is not
a bug in your code. Details: MSYS2’s environment comparison (GCC vs LLVM /
Clang
) and the
historical discussion of GCC + -lasan on MinGW
(MINGW-packages#3163).

Workaround options:

  1. MSYS2 clang64 — same machine: build Gate 2 with clang++.exe from
    C:\msys64\clang64\bin\. Use the same sanitizer flags as in this guide’s
    make sanitize recipe (-std=…, -g, -fsanitize=address,undefined,
    -fno-omit-frame-pointer, warnings). Keep ucrt64 g++ for Gate 1 if you
    want a GCC-style target build.
  2. WSL2 — run Gate 2 in a Linux layer (full sanitizer output).
  3. Dockerdocker run --rm -v .:/src gcc:12 bash -c "cd /src && make sanitize".
  4. CI — configure a Linux CI runner (GitHub Actions, GitLab CI) that runs
    Gate 2 on every push while Gate 1 and Gate 3 run locally on Windows.

Detect the linker-error pattern in a pre-push shell hook and downgrade Gate 2 to a
warning rather than a hard failure on Windows:

output=$(g++ -fsanitize=address,undefined *.cpp -o app_sanitized 2>&1)
rc=$?
if echo "$output" | grep -qE "(ld returned|collect2)"; then
    echo "WARN Gate 2: sanitizer linker error — platform limitation."
    echo "     Use Linux/WSL2, CI, or MSYS2 CLANG64 clang++ (msys2.org → Environments)."
elif [ $rc -ne 0 ]; then
    echo "FAIL Gate 2:"
    echo "$output"
    exit 1
fi

Pre-push hook (optional but recommended)

Gate 2: sanitizers (blocking on Linux; advisory on Windows)

echo “[Gate 2] Sanitizer build…”
san_out=$(make sanitize 2>&1) || true
if echo “$san_out” | grep -qE “(ld returned|collect2)”; then
echo “WARN Gate 2: sanitizer linker error (platform limitation).”
echo " Use Linux/WSL2, CI, or MSYS2 CLANG64 clang++ (msys2.org → Environments)."
elif echo “$san_out” | grep -qi “error”; then
echo “FAIL Gate 2: sanitizer build failed.”
echo “$san_out”
exit 1
fi

Platform notes

| Platform | Gate 1 | Gate 2 (sanitizers) | Gate 3 (cppcheck) |

|----------|--------|--------------------|--------------------|

| Linux | Works as-is | Full support (ASan + UBSan link and run) | Works as-is |

| macOS | Works as-is | Full support (Apple Clang supports ASan/UBSan) | Works as-is |

| Windows (MSYS2 ucrt64 g++) | Works as-is | No GNU ASan/UBSan link — see § Windows / MinGW | Works as-is |

| Windows (MSYS2 clang64) | Works as-is | Fullclang++.exe from C:\msys64\clang64\bin\ | Works as-is |