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.
By Shahid Malik