All Articles
DevOps

pre-commit Across a Django + Nuxt Monorepo (Without Linting the Wrong Files)

Shahid MalikBy Shahid MalikSeptember 4, 20266 min read

Wiring Ruff and ESLint into git hooks for Tabeer.ai's mixed Python/TypeScript repo — and the real mistake caught along the way: hygiene hooks quietly rewriting a user's plain-text notes and a screenshot that had no business being touched.

pre-commit is simple to wire up for a single-language project. Tabeer.ai is a Django backend and a Nuxt frontend in one repo, plus a requirements/ directory of the project owner's own plain-text notes, design handoff docs, and screenshots — and that last part is where the actual mistake happened, worth documenting honestly rather than glossing over.

The Config

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      - id: trailing-whitespace
        files: ^(backend|frontend)/
      - id: end-of-file-fixer
        files: ^(backend|frontend)/
      - id: check-yaml
      - id: check-added-large-files
        args: ["--maxkb=2000"]
      - id: check-merge-conflict
      - id: detect-private-key

  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.16.6
    hooks:
      - id: ruff
        args: ["--fix"]
        files: ^backend/
      - id: ruff-format
        files: ^backend/

  - repo: local
    hooks:
      - id: eslint
        name: eslint
        entry: bash -c 'cd frontend && npx eslint --fix'
        language: system
        files: ^frontend/.*\.(js|ts|vue)$
        pass_filenames: false

Two things worth pulling out specifically.

Mistake First: --all-files Doesn't Respect Your Assumptions

The first verification pass was pre-commit run --all-files — reasonable, to confirm every hook actually works before trusting it. What it also did: ran trailing-whitespace and end-of-file-fixer against every file in the repository, including a requirements/ directory of the project owner's own plain-text working notes, a couple of markdown design docs, and — more alarmingly — a PNG screenshot, which came back with a changed byte count.

The files: ^(backend|frontend)/ scoping shown above wasn't the original config — it's the fix, added after noticing git status showing changes to files nothing in this task should have touched. The PNG got reverted immediately (binary files have no business being run through text-hygiene hooks at all — thankfully harmless here, but not something to leave to chance), and the plain-text notes got reverted too, on the reasoning that "harmless whitespace cleanup" isn't the same as "in scope for this commit." Scoping the hooks to backend/ and frontend/ specifically means this can't recur — the project's own notes and design assets are simply outside what pre-commit ever looks at.

The actual lesson: --all-files is a genuinely useful verification step, but treat its output as a diff to review, not a diff to trust — especially in a repo that holds more than just the application's own source code.

Second: files: Scoping Per-Hook, Not Just Per-Repo

ruff and ruff-format are scoped to ^backend/; the local eslint hook is scoped to ^frontend/.*\.(js|ts|vue)$. Without this, every hook runs against every staged file regardless of language — Ruff attempting to lint .vue files, ESLint choking on Python. In a single-language repo this doesn't come up; in a monorepo it's the difference between a working setup and a confusing one on the very first commit.

Verify With a Full Run, Then Trust the Real (Scoped) Behavior

pre-commit install   # writes .git/hooks/pre-commit
pre-commit run --all-files
trim trailing whitespace...........Passed
fix end of files....................Passed
check yaml...........................Passed
check for added large files.........Passed
check for merge conflicts...........Passed
detect private key...................Passed
ruff (legacy alias)..................Passed
ruff format..........................Passed
eslint................................Passed

Clean, after the scope fix and after 28 pre-existing Ruff findings and 7 pre-existing ESLint errors got fixed by hand first — pre-commit doesn't create clean code, it enforces staying clean once you've gotten there.

What This Actually Buys

A commit that introduces a lint violation or an unformatted file never reaches the remote — caught locally, before CI, before a reviewer has to leave a comment about it. The real value in a mixed-language repo specifically is the scoping discipline: each hook only ever touches the files it's actually meant for, and — after the fix — only the actual application code, never the project's own notes sitting alongside it.

If you're setting up pre-commit across a multi-language repo and want the scoping done right the first time, get in touch.

Related Articles

Monitoring & Observability

Flower + Celery on Django: How I Found Silently Failing Tasks on Tabeer.ai

Users weren't getting some transactional emails. The mail-sending code looked fine, the logs weren't obviously screaming — the actual answer was Celery tasks failing with zero visibility into it. Here's the practical setup: systemd unit, basic auth, nginx subdomain, real commands.

7 min read
Shahid Malik - AI-First Odoo Consultant

Shahid Malik

AI-First Odoo ERP Specialist

Shahid Malik is an AI-first Odoo consultant helping businesses solve complex ERP and business process challenges. His work combines Odoo consulting, process optimization, automation, integrations, migrations, and practical AI solutions to build scalable and reliable business systems.

Book a consultation for your Odoo project
Discuss Your Odoo Project