# Python AI Precommit Setup

> Set up pre-commit hooks on a Python project — standard file-hygiene checks plus a security gate (gitleaks secret scanning, Trivy filesystem scan for CVEs/secrets/misconfigs, and Bandit Python SAST). Use this whenever the user wants to add, configure, or fix pre-commit hooks on a Python repo, mentions .pre-commit-config.yaml, wants secret/vulnerability/SAST scanning on commits, or is setting up code-quality guardrails — even if they just say 'add pre-commit hooks' without naming the tools. Especially for uv-based GenAI/LLM backends. Handles the setup gotchas that break first-time installs: the Trivy binary, the required data/html.tpl report template, bandit[toml] + [tool.bandit] config, and the right .gitignore entries.

- Skill: `deep-chavda/python-ai-precommit-setup` (Agent Skill)
- Install (CLI): `npx skillmds add deep-chavda/python-ai-precommit-setup`
- Raw SKILL.md: https://api.skillmd.com/api/skills/deep-chavda/python-ai-precommit-setup/raw
- Safety review: WARNING (external: skill-scanner FAIL, skillspector CAUTION)
- Works with: any agent that reads SKILL.md (Claude Code, Claude.ai, Cursor, Codex, Windsurf, 60+ more)
- Category: DevOps & Infra
- Author: Deep Chavda (https://skillmd.com/u/deep-chavda)
- Updated: 2026-07-22
- Page: https://skillmd.com/skills/deep-chavda/python-ai-precommit-setup

---


# Python Pre-commit Setup (security gate)

Wire up a Python repo with pre-commit hooks that block a commit on file-hygiene issues, leaked secrets, vulnerable dependencies, misconfigs, and unsafe Python patterns. Defaults to a **uv** project; a pip/poetry note is at the bottom.

This is a single-file skill — the config files are inlined below as code blocks you write to disk. The value isn't the YAML (pre-commit is easy); it's the four things that silently break a first run: the Trivy binary isn't a pip package, Trivy's `--template` needs a real `html.tpl` on disk, Bandit can't read `pyproject.toml` without the `[toml]` extra, and the generated report must be gitignored or it re-triggers the hook. Encode all four so the user runs `pre-commit install` once and it just works.

## What gets installed

- **pre-commit-hooks** — trailing whitespace, YAML/TOML syntax, large files, merge conflicts, leftover `pdb`/`breakpoint`, private keys, missing final newline, case conflicts.
- **gitleaks** — secret scanning; fetches its own binary via the remote repo, no manual install.
- **Trivy** (local hook) — filesystem scan for dependency CVEs (`uv.lock`), secrets, IaC/Dockerfile misconfigs. Blocks on CRITICAL/HIGH/MEDIUM; LOW ignored. Writes an HTML report.
- **Bandit** (local hook) — Python SAST for hardcoded creds, weak crypto, unsafe `subprocess`/`eval`, etc. Blocks on MEDIUM/HIGH.

## Before writing anything — resolve two variables

1. **Package/source dir** — the importable code directory (has `__init__.py`, e.g. `school_pathways_ai`, `app`, `src/<pkg>`). Ask if ambiguous. This replaces `__PACKAGE_DIR__` below.
2. **Package manager** — confirm uv (`uv.lock` + `pyproject.toml`). If pip/poetry, adapt commands (see bottom).

## Setup steps (run in order from repo root)

### 1. Dev dependencies

```bash
uv add --dev pre-commit "bandit[toml]"
```

The `[toml]` extra matters: plain `bandit` cannot read `-c pyproject.toml` and errors out — the single most common Bandit failure.

### 2. Trivy binary (NOT pip — it's a Go binary, installed system-wide)

```bash
# macOS
brew install trivy
# Debian/Ubuntu
sudo apt-get install -y wget gnupg
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | gpg --dearmor | sudo tee /usr/share/keyrings/trivy.gpg > /dev/null
echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb generic main" | sudo tee /etc/apt/sources.list.d/trivy.list
sudo apt-get update && sudo apt-get install -y trivy
# Any OS
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
```

Verify with `trivy --version`. Every machine running the hooks needs Trivy — note this in the README.

### 3. Report template + data dir

Trivy's `--format template` needs the template file to exist on disk first, or the run dies. Create `data/html.tpl` with this content:

```bash
mkdir -p data
```

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Trivy Scan Report</title>
  <style>
    body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 24px; color: #1a1a1a; }
    h1 { font-size: 22px; }
    h2 { font-size: 16px; margin-top: 28px; border-bottom: 1px solid #e5e5e5; padding-bottom: 6px; }
    table { border-collapse: collapse; width: 100%; margin: 8px 0 20px; font-size: 13px; }
    th, td { border: 1px solid #ddd; padding: 6px 8px; text-align: left; vertical-align: top; }
    th { background: #f5f5f5; }
    .sev { font-weight: 700; padding: 2px 8px; border-radius: 4px; color: #fff; white-space: nowrap; }
    .CRITICAL { background: #7c0a02; }
    .HIGH { background: #d13438; }
    .MEDIUM { background: #e8a33d; }
    .LOW { background: #6c757d; }
    .UNKNOWN { background: #9aa0a6; }
    .none { color: #2e7d32; }
    a { color: #0b57d0; text-decoration: none; }
    code { background: #f2f2f2; padding: 1px 4px; border-radius: 3px; }
  </style>
</head>
<body>
  <h1>Trivy Scan Report</h1>
  {{- $found := false }}
  {{- range . }}
    {{- if or .Vulnerabilities .Misconfigurations .Secrets }}
      {{- $found = true }}
      <h2>{{ .Target }} <small>({{ .Type }})</small></h2>
      {{- if .Vulnerabilities }}
      <table>
        <thead><tr><th>Severity</th><th>ID</th><th>Package</th><th>Installed</th><th>Fixed</th><th>Title</th></tr></thead>
        <tbody>
          {{- range .Vulnerabilities }}
          <tr>
            <td><span class="sev {{ .Severity }}">{{ .Severity }}</span></td>
            <td>{{ if .PrimaryURL }}<a href="{{ .PrimaryURL }}">{{ .VulnerabilityID }}</a>{{ else }}{{ .VulnerabilityID }}{{ end }}</td>
            <td>{{ .PkgName }}</td>
            <td><code>{{ .InstalledVersion }}</code></td>
            <td>{{ if .FixedVersion }}<code>{{ .FixedVersion }}</code>{{ else }}—{{ end }}</td>
            <td>{{ .Title }}</td>
          </tr>
          {{- end }}
        </tbody>
      </table>
      {{- end }}
      {{- if .Misconfigurations }}
      <table>
        <thead><tr><th>Severity</th><th>ID</th><th>Check</th><th>Message</th><th>Resolution</th></tr></thead>
        <tbody>
          {{- range .Misconfigurations }}
          <tr>
            <td><span class="sev {{ .Severity }}">{{ .Severity }}</span></td>
            <td>{{ if .PrimaryURL }}<a href="{{ .PrimaryURL }}">{{ .ID }}</a>{{ else }}{{ .ID }}{{ end }}</td>
            <td>{{ .Title }}</td>
            <td>{{ .Message }}</td>
            <td>{{ .Resolution }}</td>
          </tr>
          {{- end }}
        </tbody>
      </table>
      {{- end }}
      {{- if .Secrets }}
      <table>
        <thead><tr><th>Severity</th><th>Rule</th><th>Category</th><th>Title</th><th>Lines</th></tr></thead>
        <tbody>
          {{- range .Secrets }}
          <tr>
            <td><span class="sev {{ .Severity }}">{{ .Severity }}</span></td>
            <td>{{ .RuleID }}</td>
            <td>{{ .Category }}</td>
            <td>{{ .Title }}</td>
            <td>{{ .StartLine }}{{ if ne .StartLine .EndLine }}–{{ .EndLine }}{{ end }}</td>
          </tr>
          {{- end }}
        </tbody>
      </table>
      {{- end }}
    {{- end }}
  {{- end }}
  {{- if not $found }}
    <p class="none">No CRITICAL/HIGH/MEDIUM findings. ✅</p>
  {{- end }}
</body>
</html>
```

### 4. Bandit config — append to `pyproject.toml`

```toml
[tool.bandit]
exclude_dirs = ["tests", ".venv", "data", "logs", "build", "dist"]

[tool.bandit.assert_used]
skips = ["*/tests/*", "*_test.py", "test_*.py"]
```

### 5. .gitignore — the report regenerates every run; ignore it but keep the template tracked

```
data/report.html
```

Do **not** ignore all of `data/` — `data/html.tpl` must stay committed.

### 6. Write `.pre-commit-config.yaml` (replace both `__PACKAGE_DIR__`)

```yaml
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      - id: trailing-whitespace
      - id: check-yaml
      - id: check-toml
      - id: check-added-large-files
      - id: check-merge-conflict
      - id: debug-statements
      - id: detect-private-key
      - id: end-of-file-fixer
      - id: check-case-conflict

  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.21.0
    hooks:
      - id: gitleaks

  - repo: local
    hooks:
      - id: trivy-scan
        name: Trivy security scan (blocks on CRITICAL/HIGH/MEDIUM)
        entry: bash -c 'trivy fs --scanners vuln,secret,misconfig --severity CRITICAL,HIGH,MEDIUM --skip-dirs logs --skip-dirs data --skip-dirs .venv --skip-files .env --exit-code 1 --format template --template "@data/html.tpl" -o data/report.html . || { echo "❌ Trivy found CRITICAL/HIGH/MEDIUM issue(s) — open data/report.html for details."; exit 1; }'
        language: system
        pass_filenames: false
        always_run: true

      - id: bandit
        name: Bandit security scan (blocks on MEDIUM/HIGH)
        entry: uv run bandit -c pyproject.toml -r __PACKAGE_DIR__ --severity-level medium
        language: system
        pass_filenames: false
        always_run: true
```

Then pin fresh versions instead of trusting the template's: `uv run pre-commit autoupdate`.

### 7. Install and verify

```bash
uv run pre-commit install               # registers the git hook
uv run pre-commit run --all-files        # first run reformats files; fix findings; a clean second run = gate is live
```

## Verify it actually gates (optional)

- **gitleaks / detect-private-key** — stage a file with a fake `AWS_SECRET_ACCESS_KEY=...`; commit must be blocked.
- **Bandit** — add `password = "hunter2"` or `eval(user_input)` in the package; expect a MEDIUM+ block.
- **Trivy** — a known-vulnerable pinned dep in `uv.lock` should surface in `data/report.html`.

## Tuning knobs

- Too noisy? Raise Trivy `--severity` to `CRITICAL,HIGH` and Bandit to `--severity-level high` (keep both in sync).
- `src/` layout → point Bandit `-r` at `src/<pkg>`; Trivy scans `.` regardless.
- CI → run `pre-commit run --all-files` as a CI job so it's enforced even if a dev skips the local hook.
- False-positive secret → inline `# gitleaks:allow` or a `.gitleaks.toml` allowlist, not a lower gate.

## pip / poetry

Drop the `uv run` prefix and `uv add --dev`:
- **pip**: `pip install pre-commit "bandit[toml]"`; remove `uv run` from Bandit's `entry:`.
- **poetry**: `poetry add --group dev pre-commit "bandit[toml]"`; Bandit entry becomes `poetry run bandit ...`.

Trivy binary, template, and gitignore steps are identical.
