Why not just call a shell script directly? How would you use these with a CI/CD platform?
From an org standpoint you can have them (mandate?) as part of the developer experience.
(Our team doesn't use them, but I can see the potential value)
Is prek much better?
The prek documentation has a list of many large projects (such as CPython and FastAPI, to name a few) who use it; each link is a PR of how they integrated it into CI if you want to see more: https://prek.j178.dev/#who-is-using-prek
pre-commit considered harmful if you ask me. prek seems to largely be an improvement but I think it's improving on an already awful platform so you should not use it.
I know I am working on a competing tool, but I don't share the same criticism for lefthook or husky. I think those are fine and in some ways (like simplicity) better than hk.
I have a shell utility similar to make that CI/CD calls for each step (like for step build, run make build) that abstracts stuff. I'd have Prek call this tool, I guess, but then I don't get what benefit there is here.
You run the same hooks in CI as locally so it's DRY and pushes people to use the hooks locally to get the early feedback instead of failing in CI.
Hooks without CI are less useful since they will be constantly broken.
This is the kind of thing I see and I think to myself: is this solving a problem or is this solving a problem that the real problem created?
Why is your pre-commit so complicated that it needs all this? I wish I could say it could all be much simpler, but I’ve worked in big tech and the dynamics of large engineering workforces over time can make this sort of thing do more good than harm, but again I wonder if the real problem is very large engineering teams…
I'm sure this is more reliably than pre-commit, but you still have hooks building Python wheels and whatnot, which fails annoyingly often.
The VFS stuff is not quite finished yet though (it's really complicated). If anyone wants to help me with that it would be welcome!
There should be a .gitextensions in the repo that the repo owners maintain just like .gitignores and . gitattributes etc etc. Everything can still be opt in by every user but at least all git clients would be able to know about, pull down, and install per user discretion.
It seems pretty basic in this day and age but it's still a gaping hole. You still need to manually call LFS install for goodness sake.
The main advantage for me is that prek has support for monorepo/workspaces, while staying compatible with existing pre-commit hooks.
So you can have additional .pre-commit-config.yaml files in each workspace under the root, and prek will find and run them all when you commit. The results are collated nicely. Just works.
Having the default hooks reimplemented in Rust is minor bonus (3rd party hooks won't be any faster) and also using uv as the package manager speeds up hook updates for python hooks.
I think wasi is a cool way to handle this problem. I don't think security is a reason though.
Dedicated a whole chapter to it in my latest book, Effective Testing.
The trend of fast core (with rust) and convenient wrapper is great while we are still writing code.
I loathe UX flows where you get turned around. If I try to make a commit, it's because that I what I intend to do. I don't want to receive surprise errors. It's just more magic, more implicit behavior. Give me explicit tooling.
If you want to use pre-commit hooks, great! You do you. But don't force them on me, as so many projects do these days.
Now, if the server enforces checks on push, that's a project policy that should be respected.
Slow hooks are also not a problem in projects I manage as I don't use them.
> Slow hooks are also not a problem in projects I manage as I don't use them.
You bypass the slow hooks you mentioned? Why even have hooks then?
I'm advocating for JJ to build a proper daemon that runs "checks" per change in the background. So you don't run pre-commit checks when committing. They just happen in the background, and when by the time you get to sharing your changes, you get all the things verified for you for each change/commit, effortlessly without you wasting time or needing to do anything special.
I have something a bit like that implemented in SelfCI (a minimalistic local-first Unix-philosophy-abiding CI) https://app.radicle.xyz/nodes/radicle.dpc.pw/rad%3Az2tDzYbAX... and it replaced my use of pre-commit hooks entirely. And users already told me that it does feel like commit hooks done right.
Also, how do you like Radicle?
Basically what I would want is write a commit (because I want to commit early and often) then run the lint (and tests) in a sandboxed environment. if they pass, great. if they fail and HERAD has moved ahead of the failing commit, create a "FIXME" branch off the failure. back on main or whatever branch head was pointed at, if tests start passing, you probably never need to revisit the failure.
I want to know about local test failures before I push to remote with full CI.
automatic branching and workflow stuff is optional. the core idea is great.
Changes to code would obviously need to be reviewed before they are committed. That's still much better than with pre-commit, where e.g. to do simple things like banning tabs you pretty much give some guy you don't know full access to your machine. Even worse - almost everyone that uses pre-commit also uses tags instead of commit hashes so the hook can be modified retroactively.
One interesting attack would be for a hook to modify e.g. `.vscode/settings.json`... I should probably make the default config exclude those files. Is that what you meant? Even without that it's a lot more secure than pre-commit.
Pre-commit checks should be opt-in with CI as the gate. It's useful to be able to commit code in a failing state.
It's as simple as a script with a cp command that I run after any clone of a repo that requires it; certainly doesn't require anything as elaborate as a hook manager.
I personally can't stand my git commit command to be slow or to fail.
[0]: such as https://github.com/watchexec/watchexec
i regularly edit history of PRs for a variety of reasons and avoid pre-commit when possible.
put it all in CI thank you please — gimme a big red X on my pipeline publicly telling me i’ve forgotten to do something considered important.
The checks in those pre-commit hooks would need to be very fast - otherwise they'd be too slow to run on every commit.
Then why would it save time and money if they only get run at the pipeline stage? That would only save substantial time if the pipepline is architected in a suboptimal way: Those checks should get run immediately on push, and first in the pipeline so the make the pipeline fail fast if they don't pass. Instant Slack notification on fail.
But the fastest feedback is obviously in the editor, where such checks like linting / auto-formatting belong, IMHO. There I can see what gets changed, and react to it.
Pre-commit hooks sit in such a weird place between where I author my code (editor) and the last line of defense (CI).
That’s reversing the flow of control, but might be workable!
* CI (I understand pre-commit shifts errors left)
* in editor/IDE live error callouts for stuff like type checking, and auto-formatting for things like "linters".
Do you run tests? How do you know _which_ tests to run, and not just run every test CI would run, which could be slow?
I haven't yet submitted it to upstream for design discussion, but I pushed up my branch[1]. You can also declare a revset that the target revision must match, for extra belts and suspenders (eg., '~conflicts()')
[1] https://github.com/paulsmith/jj/tree/protected-bookmarks
I can't, because the point of our pre-commit use isn't to run logic in hooks that can't be run otherwise.
e.g. We use pre-commit to enforce that our language's whitespace formatting has been applied. This has the same configuration in the IDE, but sometimes devs ignore IDE warnings or just open files in a text editor for a quick edit and don't see IDE warnings or w/e.
"Replaced by CI" isn't really meaningful in our context - pre-commit is just a tool that runs as part of CI - some things get done as pre-commit hooks because they're fast and it's a convenient place to put them. Devs are encouraged to also run pre-commit locally, but there's no enforcement of this.
> Do you run tests? How do you know _which_ tests to run, and not just run every test CI would run, which could be slow?
We have performance metrics for pre-commit hooks and pre-push hooks. I forget the exact numbers, but we want stuff to "feel" fast, so e.g. if you're rebasing something locally with a few dozen commits it should only take seconds. Pre-push hooks have a bit more latitude.
That's still multiple minutes compared to an error thrown on push - i.e. long enough for the dev in question to create a PR, start another task, and then leave the PR open with CI failures for days afterwards.
> But the fastest feedback is obviously in the editor, where such checks like linting / auto-formatting belong, IMHO.
There are substantial chunk of fast checks that can't be configured in <arbitrary editor> or that require a disproportionate time investment. (e.g. you could write and maintain a Visual Studio extension vs just adding a line to grep for pre-commit)
SelfCI is _very_ minimal by design. There isn't really all that much to document other than what is described in the README.
> Also, how do you like Radicle?
I enjoy that it's p2p, and it works for me in this respect. Personally I disagree with it attempt to duplicate other features of GitHub-like forge, instead of the original collaborate model of Linux kernel that git was built for. I think it should try to replicate something more like SourceHut, mailinglist thread, communication that includes patches, etc. But I did not really _collaborated_ much using Radicle yet, I just push and pull stuff from it and it works for that just fine.
I'm not sure if I fully understood. But SelfCI's Merge-Queue (mq) daemon has a built-in hook system, so it's possible to do custom stuff at certain points. So probably you should be able to implement it already, or it might require couple of minor tweaks (should be easy to do on SelfCI side after some discussion).
If it’s on a pull/merge request, you’re wasting reviewer time.
If the hook is blocking secrets, you can’t un-push it with 100% certainty so you have to revoke credentials.
For texts, I tend to have the equivalent of “pytest tests/unit/“ since those are fast and a good sanity check, especially for things like refactoring.
I also run our pre-commit checks in CI for consistency so we’re never relying on someone’s local environment (web editors exist) and to keep everyone honest about their environment.
I don't recommend it, though, at least not on large repositories. Too much opportunity to collide with command-line jj write operations.
But what I didn't pick up for a quick scan of README is best pattern for integrating with git. Do you expect users to manually run (a script calling) selfci manually or is it hooked up to git or similar? When does the merge hooks come into play? Do you ask selfci to merge?
Is enough to don't even open the link! Everything right now seems to have an urgent need to be developed into Rust, like why???
Just like kubernetes, many companies followed the kubernetes hype even when it was not needed and added unnecessary complexity to a simple environment.
Now it is Rust time!!
I, too, want checks per change in jj -- but (in part because I need to work with people who are still using git) I need to still be able to use the same checks even if I'm not running them at the same point in the commit cycle.
So I have an alias, `jj pre-commit`, that I run when I want to validate my commits. And another, `jj pre-commit-branch`, that runs on a well-defined set of commits relative to @. They do use `pre-commit` internally, so I'm staying compatible with git users' use of the `pre-commit` tool.
What I can't yet do is run the checks in the background or store the check status in jj's data store. I do store the tree-ish of passing checks though, so it's really quick to re-run.
So reviewers have to digest all of the twists and turns I took to get to the final result? Why oh why oh why?
Sure, if they've already seen some of it, then there should be an easy way for them to see the updates. (Either via separate commits or if you're fortunate enough to have a good review system, integrated interdiffs so you can choose what view to use.)
In a better world, it would be the code author's responsibility to construct a meaningful series of commits. Unless you do everything perfectly right the first time, that means updating commits or using fixup commits. This doesn't just benefit reviewers, it's also enormously valuable when something goes wrong and you can bisect it down to one small change rather than half a dozen not-even-compiling ones.
But then, you said "atomic", which suggests you're already trying to make clean commits. How do you do that without modifying past commits once you discover another piece that belongs with an earlier step?
> You just squash on merge.
I'd rather not. Or more specifically, optimal review granularity != optimal final granularity. Some things should be reviewed separately then squashed together (eg a refactoring + the change on top). Some things should stay separate (eg making a change to one scary area and then making it to another). And optimal authoring granularity can often be yet another thing.
But I'll admit, git + github tooling kind of forces a subpar workflow.
It's a little surprising that git doesn't pass pre-commit hooks any information, like a list of which files were changed in the soon-to-be-made commit. git does so for pre-push, where it writes to a hook's stdin some information about the refs and remotes involved in the push.
I wonder if many pre-commit hooks, like the kind which run formatters, would be better off as `clean` filters, which run on files when they are staged. The filter mechanism makes it easier to apply just to the files which were changed. In the git docs, they even use a formatter (`indent`) as an example.
https://git-scm.com/book/ms/v2/Customizing-Git-Git-Attribute...
Are you asking what advantage pre-commit has over a shell script?
Mostly just functionality: running multiple hooks, running them in parallel, deciding which hooks to run based on the commit files, "decoding" the commit to a list of files, offering a bunch canned hooks, offering the ability to write and install non-shell hooks in a standard way.
I agree it’s better, but not because of wasi
This is a long standing sore point in pre-commit, see https://github.com/pre-commit/pre-commit/issues/860 and also linked duplicates (some of which are not duplicates).
Pre-commit and pre-push hooks serve the purpose of keeping code isolated to a developer's machine. This is a recipe for disaster. You will run into situations where important work isn't accessible since a developer couldn't commit/push their code and the machine was lost or damaged. I've seen it happen.
The benefit to many of having them as a hook is that you discover it's broken before you pushed your changes, and not when you finally get around to checking the CI on your branch and realising it failed after 30s.
There is of course no reason why you have to have it installed as a precommit hook - many people prefer to run it manually, and the pre-commit tool/prek allows for that.
IMO if there’s a formatting issue, and the tool knows how it should look, it should fix it for you.
For some languages there are some rather slow hooks, and using it on a big monorepo can take a while (a full run across my work's main repo takes minutes). If you update python based hooks all the time then installing and creating the virtualenvs can be slow too which prek speeds up.
https://devenv.sh/blog/2025/11/26/devenv-111-module-changelo...
Just to make clear it's been written -- sorry, ENGINEERED -- in Rust.
But that's the whole point of locally checking the code, no? Would you prefer to commit broken things, fix them and then rebase and squash each time?
From the docs I think Limmat is much more minimal. It doesn't have a merge queue or anything, "jobs" are just commands that run in a worktree.
I would be interested to try SelfCI coz I have actually gone back and forth on whether I want that merge queue feature in Limmat. Sometimes I think for that feature I no longer want it to be a local tool but actually I just want a "proper CI system" that isn't a huge headache to configure.
If you use a tool like this via Devenv instead of using its built-in mechanisms for installing tools:
- you can add a linter without putting it on your path
- you can put a linter on your path without enabling any git hooks for it
- if you are already using a linter in a git hook, adding it to your environment will get you the exact same version as you're already using for your git hook at no additional storage cost
- if you are already using a linter at the CLI, and you add a git hook for it, your hook will run with the exact same version that you are already using at the CLI at no additional storage cost
- your configuration interface is isomorphic to the upstream one, so
- any custom hooks you're already using can be added without modification beyond converting from one format to another;
- any online documentation you find about adding a custom hook not distributed with the upstream framework still applies;
- and you can configure new, custom, or modified hooks with a familiar interface.
- any hook you write as a script or with substantial logic can also be plugged into a built-in task runner for use outside git hook contexts, where
- you can express dependency relationships between tasks, so
- every task runs concurrently unless dependency relations mandate otherwise.
Which imo solves that problem pretty well. My team uses that kind of setup in all of our projects.> The interface isn't built with parallelism in mind, it's sort of bolted on but not really something I think could work well in practice.
I'm curious about what this means. Could you expand on it?
You might be interested in something like `treefmt`, which is designed around a standard interface for configuring formatters to run in parallel, but doesn't do any package management or installation at all:
https://github.com/numtide/treefmt
(That might address both of the issues I've replied to you about so far, to some extent.)
> It also uses a bunch of rando open source repos which is a supply chain nightmare even with pinning.
If the linters you're running are open source, isn't this what you're ultimately doing anyway? Nix gives a bit more control here, but I'm not sure whether it directly addresses your concern.
> I am working on a competing tool
Oh, dammit. Only after writing all of this out did I realize you're the author of mise. I'm sure you're well aware of Nix and Devenv. :)
Because I think your critiques make sense and might be shared by others, I'll post this comment anyway. I'm still interested in your replies, and your opinion of having some environment management tool plug into prek and supplant its software installation mechanisms.
And because I think mise likely gets many of these things right in the same way Devenv does, and reasonable people could prefer either, I'll include links to both Devenv and mise below:
> you can't force folks to run them
I think it's useful to be able to disable things like this when debugging or reconfiguring them. I sometimes disable the ones I've set up for myself, then reenable them later.
> Why not just call a shell script directly?
Because it's manual; I have to remember to do it each time. But there's no reason not to have a script you can invoke in other ways, if that's something you want.
> How would you use these with a CI/CD platform?
The thing that sets up your environment also installs the git hooks configuration, and/or you can have it invoke specific hooks via CLI.
But if you don't want to have hooks modify code, in a case like this you can also just use `tofu validate`. Our setup does `tflint` and `tofu validate` for this purpose, neither of which modifies the code.
This is also, of course, a reasonable place to have people use `tofu plan`. It you want bad code to fail as quickly as possible, you can do:
tflint -> tfsec -> tofu validate -> tofu plan
That'll catch everything Terraform will let you catch before deploy time— most of it very quickly— without modifying any code.
- very fast? run it all the time (shell prompt drawing, if you want, like direnv)
- fast? run it in a pre-commit hook
- a bit slow? run it in a pre-push book
- really slow? run it in CI, during code review, etc.
Fwiw: I also rewrite history often-ish but it's never that fast for me because I have commit signing turned on and that requires verifying my presence to a USB smartcard on each commit. For me, it's okay if a commit takes a second or two. As it creeps up beyond 3 or 4 seconds, I become increasingly annoyed. If a commit took a minute I would consider that broken, and if I were expected to tolerate that or it were forced on me, I'd be furious.I generally expect an individual pre-commit hook to under ~200ms (hopefully less), which seems reasonable to me. Some of the ones we have are kinda slow (more than 1s) and maybe should be moved to pre-push.
Since you seem especially sensitive to that latency, here's what I'd propose if we worked together:
If you own a repo, let's make all the hooks pre-push instead of pre-commit. On my repos, I like many hooks to run pre-commit. But since the hooks we use are managed by a system that honors local overrides via devenv.local.nix, let's make sure that's in .gitignore everywhere. When I'm iterating in your codebases and I want more automated feedback, I'll move more hooks to pre-commit, and when you're working in mine you can move all my hooks to pre-push (or just disable them while tidying up a branch).
If you look at hk you will understand what I'm talking about in regards to parallelism. hk uses read/write locks and other techniques like processing --diff output to safely run multiple fixers in parallel without them stomping on each other. treefmt doesn't support this either, it won't let you run multiple fixers on the same file at the same time like hk will.
> If the linters you're running are open source, isn't this what you're ultimately doing anyway?
You have to trust the linters. You don't also need to trust the plugin authors. In hk we don't really use "plugins" but the equivalent is first-party so you're not extending trust to extra parties beyond me and the lint vendors.
That sounds pretty cool! I will definitely take a closer look.
* in-editor, real time linting / formatting / type checking. This handles whatever file you have open at the time.
* pre-commit, do quick checks for all affected code - linting, type checking, formatting, unit tests.
* CI server, async / slow tests. Also does all the above (because pre-commit / pre-push scripts are clientside and cannot be guaranteed to run), plus any slower checks like integration tests.
Basically "shift left", because it takes 100x as long to find and fix a typo (for example) if you find it in production compared to in your editor while writing.
I feel the same way but you can have hooks run on pre-push instead of pre-commit. This way you can freely make your commits in peace and then do your cleanup once afterwards, at push time.
We were coming from a an application perspective where blocking the users intent is a no-go.
Do you have a link to a discussion where the JJ community is discussing checks?
It doesn't need Landlock because WASI already provides a VFS.
That's reasonable. My personal (and that of my team at the time) take was that I was willing to let formatting - and only formatting - be auto-merged into the commit, since that isn't going to impact logic. For anything else, though, I would definitely want to let submitter review the changes.