Pre-Commit Hook That Stops AI-Coder Bugs

Set up BrassCoders as a pre-commit hook to block hardcoded secrets, SQL injection, and hallucinated imports before they hit your git history — 5 lines of config.

Copper Sun Brass Team · · 7 min read
oss-coresecurityai-code-review

The commit that ships a hardcoded API key looks like every other commit. Pre-commit hooks exist precisely because code review happens too late — the key is already in the diff, already in the author’s mental model as “placeholder I’ll fix,” already about to become history. The hook that runs before the commit is the earliest possible catch.

AI coding assistants make this worse, not better. Cursor and Claude Code generate code fast. They also generate hardcoded credentials, SQL queries built with f-strings, and subprocess(shell=True) calls with user-controlled input. The assistant doesn’t second-guess itself. The pre-commit hook should.

What a Pre-Commit Hook Actually Does

A pre-commit hook is a shell script that runs automatically before git commit finalizes. If the script exits non-zero, the commit is blocked. The pre-commit framework manages hook installation, versioning, and configuration via a .pre-commit-config.yaml file checked into the repository — so every developer on the team gets the same hooks, pinned to the same versions, without a setup wiki page.

The framework handles the plumbing. You write the configuration; it wires the hook into .git/hooks/pre-commit. When you run git commit, the framework activates each configured hook in sequence and blocks the commit if any hook exits non-zero.

That exit code is the only contract. Your hook doesn’t need to produce structured output or a JSON report. Exit 0, commit proceeds. Exit 1, commit stops. Simple.

What the framework adds beyond a plain shell hook: cross-platform consistency, language-specific isolated environments, automatic hook installation when a new developer clones the repo, and a clean multi-hook runner that outputs each hook’s name and pass/fail status. Without it, hooks are an informal per-developer convention. With it, they’re a project-level policy that survives team turnover.

Why BrassCoders Works Well as a Pre-Commit Hook

BrassCoders runs 12 scanners — Bandit, Pylint, Pyre/Pysa, Semgrep, ast-grep, detect-secrets, and six custom detectors — with a single command, makes zero network calls when run with --offline, and exits with code 1 on any CRITICAL finding. That exit code is exactly what pre-commit needs to block a commit.

Without a unified scanner, a typical .pre-commit-config.yaml accumulates separate entries for Bandit, detect-secrets, Semgrep, and custom scripts — each with its own version pin, its own invocation flags, its own output to interpret. They don’t share severity definitions, so “high” in Bandit and “high” in detect-secrets mean different things. Running BrassCoders consolidates those into one hook with one exit signal.

The --offline flag matters here specifically. Pre-commit hooks run on every commit. You want them to finish in two seconds, not depend on a network call that might time out. brasscoders --offline scan is enforced offline at the API level — it exits non-zero if any outbound call is attempted, making the guarantee hard rather than aspirational.

Setting Up the Hook (5 Lines)

BrassCoders’s pre-commit configuration requires five lines in .pre-commit-config.yaml and one install step. The pass_filenames: false option tells pre-commit to pass the whole project directory to BrassCoders rather than individual staged files — which is necessary because BrassCoders performs project-level analysis, not file-by-file scanning.

Install both tools first:

# Install pre-commit (if not already installed)
pip install pre-commit

# Install brasscoders
pip install brasscoders==2.0.8

Then create or update .pre-commit-config.yaml at your project root:

repos:
  - repo: local
    hooks:
      - id: brasscoders
        name: BrassCoders scan
        entry: brasscoders --offline scan
        language: system
        pass_filenames: false
        stages: [pre-commit]

Activate the hook and verify it runs:

# Install the hook into .git/hooks/pre-commit
pre-commit install

# Test it (runs hooks on all files without committing)
pre-commit run --all-files

The pre-commit run --all-files command is the right first test. It exercises the full scanner against your existing codebase before you’ve committed anything, so you see what would block a commit. Run it once, fix the CRITICALs, then the hook is calibrated to your project. The pre-commit documentation covers additional configuration options, including per-hook environment isolation and caching.

When the hook runs on a commit, you’ll see output from the pre-commit runner followed by BrassCoders’s finding summary. A clean scan looks like BrassCoders scan...Passed. A blocked commit shows the CRITICAL findings inline and exits with a non-zero code before git writes the commit object. The developer sees the blocked finding, addresses it, and re-commits. Nothing reaches the repository.

What Gets Blocked (and What Doesn’t)

BrassCoders’s pre-commit hook blocks commits containing CRITICAL findings: hardcoded credentials in 20+ formats (AWS access keys, GitHub PATs, OpenAI API keys, Stripe live keys, JWTs, PEM-encoded private keys), SQL injection via f-string formatting, command injection via subprocess(shell=True) with user input, and imports of packages that don’t exist on PyPI.

The secret detection draws on Yelp’s detect-secrets as the upstream library. detect-secrets uses entropy analysis and pattern matching to catch credentials that look like secrets, not just credentials that match a specific regex. BrassCoders adds 20+ additional patterns on top of the detect-secrets base — format-specific matchers for AWS access key prefixes, GitHub token prefixes, Stripe live-key prefixes, OpenAI key prefixes, and others that entropy analysis alone would rate as ambiguous. The combination catches both the high-entropy unknown-format key and the low-entropy but structurally obvious sk_live_... Stripe key.

The hallucinated-import detector deserves specific attention for AI-assisted codebases. AI code generators hallucinate package names. Your assistant writes from langchain_community.llms import SomeLLM and the package exists — but it also writes from langchain_extensions.tools import ContextualSearchChain and that package does not exist anywhere on PyPI. BrassCoders’s phantom-import detector catches these before they land in your requirements.txt and break CI for the next person who checks out the branch.

Non-critical findings — lower-severity code-quality issues, style patterns, informational notices — are written to .brass/ and don’t block the commit. The developer can review the YAML output at any time with brasscoders report. The hook fires only on ship-blockers. The OSS core is at github.com/CopperSunDev/brasscoders.

Pre-Commit vs CI: Running Both

BrassCoders runs at both stages of a healthy pipeline — locally as the pre-commit hook and remotely in CI — and the two gates do different jobs. The pre-commit hook catches issues at the lowest cost: nothing is in the repository yet, no one else has pulled the code, no CI minutes consumed. The CI run on every push produces the .brass/detailed_analysis.yaml artifact that becomes the auditable record attached to the pull request.

The recommended setup runs brasscoders --offline scan at both stages:

git commit (pre-commit) → git push → CI (GitHub Actions)
     ↓                                      ↓
BrassCoders blocks            BrassCoders uploads
CRITICAL findings             .brass/ as PR artifact

Pre-commit blocks the fast-and-cheap catches. CI catches anything that slipped through a --no-verify bypass and produces the artifact your security audit needs. Neither replaces the other.

A developer who bypasses the pre-commit hook to push a WIP commit still gets caught by CI before the PR merges. A CI-only setup catches the bug after two people have already reviewed the diff. The combination keeps the feedback loop short for the common case.

Skipping the Hook When You Need To

Pre-commit hooks can be bypassed with git commit --no-verify. The flag exists for good reasons — WIP stash commits, emergency hotfixes where you’re iterating fast, commits to scratch branches that will be squashed. The bypass is a feature, not a failure.

Document your team’s policy explicitly. The recommended pattern: bypass is allowed for WIP commits to feature branches, never for commits to main or release branches. Write it in your CONTRIBUTING guide so it’s not an informal norm that someone ignores under pressure.

A .brassignore file at your project root also controls which paths BrassCoders scans. If a directory contains intentional test fixtures with placeholder credentials — a tests/fixtures/ folder full of fake AWS keys used to verify detection — add that path to .brassignore so those don’t trigger the hook on every commit. The hook then applies to real source code while test fixtures stay exempt.

The CI gate is the backstop. Any commit that bypasses the pre-commit hook still hits brasscoders --offline scan in CI before it can merge. The two-gate setup means a --no-verify bypass defers the check, not cancels it.


Install BrassCoders: pip install brasscoders==2.0.8. Apache 2.0 licensed, no account required, runs fully offline.

For the CI half of this setup, see the GitHub Actions configuration post.

Frequently Asked Questions

Does BrassCoders work as a pre-commit hook?

Yes. BrassCoders exits with code 1 on any CRITICAL finding, which is exactly the signal pre-commit needs to block a commit. Configure it with five lines in `.pre-commit-config.yaml` and run `pre-commit install` — the hook runs automatically on every `git commit` from that point forward.

Why use pass_filenames: false in the pre-commit config?

BrassCoders performs project-level analysis across all 12 scanners, not file-by-file linting. `pass_filenames: false` tells pre-commit to pass the full project directory to BrassCoders instead of staging individual changed files — which is necessary for secret correlation, taint-flow analysis, and hallucination detection to work correctly.

What does BrassCoders block at commit time?

BrassCoders blocks commits containing CRITICAL findings: hardcoded credentials in 20+ formats (AWS access keys, GitHub PATs, OpenAI API keys, Stripe live keys, JWTs, PEM-encoded private keys), SQL injection via f-string formatting, command injection via subprocess with shell=True and user input, and imports of packages that don't exist on PyPI — hallucinated dependencies from AI code generation.

Does the --offline flag matter for pre-commit use?

Yes, and it's the right default. `brasscoders --offline scan` makes zero outbound network calls and exits non-zero if a network call is attempted. For a pre-commit hook that runs on every commit, you want deterministic local-only behavior — no timeouts, no network dependency, no accidental data egress.

Can I bypass the hook when needed?

Yes. `git commit --no-verify` bypasses any pre-commit hook. Document your team's policy for when this is acceptable — the recommended pattern is: bypass is allowed for WIP commits to feature branches, never for commits to main or release branches. The CI gate catches anything the pre-commit hook misses.

Does BrassCoders pre-commit replace Bandit and detect-secrets hooks separately?

Effectively yes. BrassCoders orchestrates Bandit, detect-secrets (Yelp), Pylint, Pyre/Pysa, Semgrep, ast-grep, and six custom detectors under one command. Running BrassCoders replaces what would otherwise be six separate hook entries in your `.pre-commit-config.yaml`, each with its own version pinning and output format.