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 ifmakereports “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:
- WSL2 — run Gate 2 in a Linux layer (recommended; full sanitizer output).
- Docker —
docker run --rm -v .:/src gcc:12 bash -c "cd /src && make sanitize". - 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 -Wextraoutput, 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.