Using pre-commit as a polyglot task runner: elegant or kludgy?
Posted by dangerousdotnet@reddit | Python | View on Reddit | 38 comments
Package maintainers: does your Makefile or justfile delegate tool calls to pre-commit (e.g., for your lint all targets)?
I see a lot of modern repos doing it this way now. On one hand, I can see the elegance of it—pre-commit has basically evolved into an execution engine that manages isolated polyglot tool environments.
But it still just feels weird to me, almost like a layer violation. Maybe it's just because my mental model still views pre-commit strictly as a git-hook manager rather than what it has actually become.
One huge benefit, I guess, of having linting and quality tools delegated this way is parity: your CI pipeline, your pre-commit hooks, and your manual justfile targets all run the exact same tools in the exact same way.
It also solves the polyglot problem. Modern dev dependencies can't all be managed by one tool (like uv, pip, or pnpm) because some are Node, some are Python, some are Go, etc.
Curious to hear how others are approaching this. Any strong reasons for / against delegating to pre-commit for your task runners vs. keeping them strictly decoupled?
TheseTradition3191@reddit
been burned by the pre-commit-as-task-runner pattern. the issue is pre-commit creates isolated envs per hook, which is great for reproducibility but slow when you just want to run ruff on its own.
ended up with the inverse: justfile defines the commands, pre-commit calls the justfile targets. you get the git-hook integration without coupling your task definitions to pre-commit's env management.
thats also the mental model issue - pre-commit hooks run on changed files by default, justfile targets run on everything. once you're using pre-commit for both, lint-all and commit-lint end up sharing config in ways that trip you up
aminoy77@reddit
Kludgy for me. The mental model mismatch is real — every time I see pre-commit used as a task runner I have to context-switch from "git hook" to "execution engine" and it never feels clean.
For polyglot I just use a plain Makefile with explicit calls to each tool. More verbose, zero magic, anyone can read it without knowing pre-commit's config format.
The parity argument is the strongest case for it though. If your CI and hooks are running different versions of the same linter you're going to have a bad day eventually.
dangerousdotnet@reddit (OP)
How do you manage the installation of tools?
aminoy77@reddit
For HelloChusquis I went plain Makefile + each tool declares its own deps. No pre-commit involvement.
The polyglot problem doesn't hit me much since it's pure Python, but for mixed stacks I'd probably still keep pre-commit strictly for hooks and use a separate task runner. Mixing the two feels like it'll bite you when someone new joins and assumes pre-commit = hooks only.
wpg4665@reddit
I do it the other way around...pre-commit and CI both call the Makefile targets
readonly12345678@reddit
Yeah, just have precommit wire up for… precommit
Glad_Friendship_5353@reddit
I do this way. Work great. Instead of Makefile or justfile, I use bakefile for reusability across repo.
So, not just lint command. I manage in a way that build, deploy, publish or any common commands work exactly the same way in both ci and local machine.
Noobfire2@reddit
Yes. I find pre-commit very nice as a concept, but really weirdly executed. I'd rather have fully descriptive definitions of what shall be linted in which way in my justfile, because that's actually designed to be directly executed.
Ontop of that, I just need a simple layer that ensures that these command return 0 before committing. Nowadays even the new native git hooks could do that instead of pre-commit.
dusktreader@reddit
This is the way
Individual-Brief1116@reddit
I've been doing this for about a year now and honestly it works pretty well. The mental shift took some time but the parity argument sold me. Nothing more frustrating than having your local linting pass but CI fail because they're running different configs or versions. Pre-commit basically became a package manager whether we like it or not.
saucealgerienne@reddit
kludgy but I keep doing it anyway.
the mental model breaks once you start using it for things that aren't commit checks. hooks run sequentially, no dependency graph, and debugging a failing "task" is way less intuitive than a Makefile or just call.
the appeal is that the config is already there and teammates already have it installed. for linting on demand or a quick formatting pass it works fine. for anything that actually has to run in order with real dependencies I move it to a Makefile pretty quickly.
dangerousdotnet@reddit (OP)
Yup, that's pretty much where I landed. Been meaning to check out Mise for projects I work on where there's a core team that can be convinced to all use the same tooling
tunisia3507@reddit
Whenever precommit is mentioned, it's worth bringing up prek, a rust rewrite which can use the same config file but provides a massive speedup, and is a single binary to install.
Physical_Drawer6740@reddit
Also prek is maintained by people who seem to enjoy what they're doing and have so far seemed capable of interacting politely with their users in a wide variety of circumstances.
ProsodySpeaks@reddit
I keep seeing comments like this so I guess I missed something.
What did Anthony asolittle do that pissed everyone off?
I only know him from YouTube and massively appreciate his contribution there, but it seems clear he's done something annoying?!
rghthndsd@reddit
Read some closed issues on pre-commit's tracker and decide for yourself.
ProsodySpeaks@reddit
Thanks. Are there loads of examples? Or can you link one in particular?
Physical_Drawer6740@reddit
Loads of examples across all of his repos. Most of his closed issues. Basically every time toml comes up for one example. It's not that I think the guy has done anything actively wrong, he's just consistently unnecessarily abrasive in a way that seems like he doesn't actually enjoy maintaining his projects or participating in open source software. And I get that users can be entitled and clueless, but when the 3rd or 4th person comes in and asks the same (valid) question, most people would take that as a sign to put the answer somewhere a little more obvious than a long issue thread that's unreadable because it was poorly copied from a different platform.
The tools themselves are fine, there are just now equivalent or better replacements for all of them where I'm not scared to interact with the maintainers.
pip_install_account@reddit
it doesn't provide a massive speed up
ProsodySpeaks@reddit
Umm what?
Have you got some benchmarks?
Because that's a pretty outlandish statement.
wpg4665@reddit
If you set a
priorityon all your hooks that are set at the same number, prek will run them all in parallel. That's where you see the speedup!tunisia3507@reddit
It does for running the built-in hooks, and it does for setting up python-based hooks because it uses uv to manage the environments.
pip_install_account@reddit
How many times during the day you "set up" hooks? 10? 100?
tunisia3507@reddit
Every time CI runs, at which point I benefit from uv's superior caching.
dangerousdotnet@reddit (OP)
TIL! Never heard of it before, going to check it out. https://github.com/j178/prek
ProsodySpeaks@reddit
Prek is supposed to be a drop-in replacement for pre-commit. AFAIK if your flow works with p-c it will work with prek with zero changes. Like literally the same p-c.yml is fine in prek.
M4mb0@reddit
prek ftw. Some people also seem to be switching to lefthooks rather than pre-commit.
inigohr@reddit
I have seen that before but I do not see the benefit. If anything what makes more sense to me is to have just or make be polyglot and pre-commit is just a thin wrapper calling those.
And I don't see how pre-commit is any more language agnostic than make or just, they're all just shell wrappers.
dangerousdotnet@reddit (OP)
It's not that pre-commit is more language agnostic, it's that pre-commit is an actual isolated package manager. A typical makefile or justfile assumes you've got tool X at whatever version on your PATH and it just picks it up from there.
shadowdance55@reddit
That's easily handled by uvx.
dangerousdotnet@reddit (OP)
No. uvx bypasses your lockfile and any project-wide min package age settings you have. You'd have to add a command line option to every single uvx invocation and it's easy to miss one.
ProsodySpeaks@reddit
Pretty sure we can say
uv tool install http://www.github.com/user/repo? Or any other resolution logic uv uses -from path, pypi, local git, github, etc?inigohr@reddit
Looking at your other comments, the main place my assumption breaks down is dependencies whose runtime you don't manage natively in your project, such as Go binaries for specific commands (actionlint) in a non-Go project.
The one time I had to do that in a project I looked at the
aquapackage manager.miseuses this registry for many of its available tools.miseis probably the closest thing to a polyglot manager. I used it for a year or two beforeuvarrived to manage my python version installations (it worked much better thanpyenvand the like) but I bounced off it for the same reason I wouldn't choose pre-commit as my centralized version manager, I just prefer to let individual tools handle what they do best.dangerousdotnet@reddit (OP)
Yeah mise looks awesome. Spmeone I know just mentioned it to me. Not sure if I'd force it on someone just trying to clone and submit a small PR but for big monorepos it looks badass. I've been hacking my own config together with chezmoi and volta and pyenv.
inigohr@reddit
I see what you mean, but working with that assumption you can remove the need for a centralized package manager no?
my experience extends only to python and JS, so if there's other ecosystems where this breaks down it's a lack of visibility from me but: let's assume you have a project that requires python dependencies and npm. if you configure uv and npm with their respective config files, a shell wrapper like make or just (or even pre-commit) can just work under the assumption that the required binaries exist in the current path, because each of your individual tools take care of managing that. and uv and npm both work in CI the same way they work locally, so as long as you have pyproject.toml (and preferably uv.lock) and package.json, you're already ensuring dependencies match.
in the end, pre-commit seems to just do that behind the scenes, at least for python I know it's just a thin wrapper around pip install commands and .venv. I prefer having control over where things are running directly and composing things rather than trying to have one centralized management system.
I will admit, my experience using pre-commit doesn't go beyond simple linting commands, but I've never felt the need to reach for it for anything OTHER than pre-push hooks.
Traditional_Train625@reddit
been using pre-commit this way for about year now and its actually pretty solid once you get past mental block
the parity thing you mentioned is huge - nothing worse than having ci fail because your local lint command runs different version or config than what pre-commit uses. drove me crazy before i switched everything over
sure it feels bit weird at first using it outside of git hooks but pre-commit basically became package manager for dev tools whether we admit it or not. like you said with polyglot problem - trying to manage python linters nodejs formatters and go tools all separately is pain in the ass
only downside i found is when pre-commit itself breaks or has issues then your whole dev workflow gets stuck. happened to me once when their cache got corrupted and took me while to figure out what was going on
dangerousdotnet@reddit (OP)
Yep. I have all my CI and pre-commit tools SHA-pinned for security reasons anyway, so I'm not as worried about pre-commit changing out from under me. Seen too many postmortems from 2025-2026 where package maintainers get popped because an attacker was able to move a tag on a tool that's not frozen in a lockfile.
The drawback for me of moving all the tools into the pre-commit YAML is I'd have to SHA-pin them there, which is sort of annoying when you want your toml file to be the "front door" of the project where everyone can see what package versions you're using for everything. They'd have to go dig inside the pre-commit yaml inside a hidden directory to figure out what versions I'm SHA-pinned to.
I have `renovate` managing my SHA-pinned ratchets with a cooldown / min-age period, so it's not like I have to manually maintain version SHA's, I just run renovate and review its PR. But still...
ProsodySpeaks@reddit
Is there no way to unify pyproject.toml with precommit versions?
There should be a way to automate this. Pr on prek to get versions from toml in a kinda java/kotlin fashion?