Important
This repo has been archived. Development continues in the
zireael monorepo at
tools/jj-hooks/.
- Install:
brew install mattwilkinsonn/zireael/jj-hooks(orcargo install jj-hooks— the crate stays on crates.io). - Issues / PRs: open them against
mattwilkinsonn/zireael with
jj-hooksin the title. - Old tap:
mattwilkinsonn/tapis also archived. The new tap path ismattwilkinsonn/zireael.
Run pre-commit, prek, lefthook, or hk hooks against jj bookmark pushes — with full support for secondary jj workspaces.
Ships as two binaries: jj-hooks (canonical name) and jj-hp (shorter alias
that's easier to type and works with shell completion). Pick the one you like;
they're identical.
jj-hp push is a drop-in replacement for jj git push:
- Asks jj which bookmarks the push would update on the remote.
- For each bookmark being added or moved, creates an ephemeral detached git worktree at the target commit and runs the configured hook backend there.
- If hooks fail or modify files, the push is aborted. Modifications get
committed as a fixup commit whose hash is printed so you can
jj squashthe fixes into your target or inspect them withjj show. - If everything passes cleanly, executes the real
jj git push.
jj-hp run [REVSET] runs hooks against a revset without pushing — useful for
"lint this change before I move on" workflows.
Earlier jj + pre-commit integrations ran hooks in the user's working copy,
which doesn't work from a secondary workspace: the worktree is the secondary's
files but the git index lives in the primary's .git, so pre-commit's
".pre-commit-config.yaml is unstaged" check fires every time.
jj-hooks sidesteps this entirely by running every hook in a fresh
git worktree add --detach checkout of the target commit. The user's
working copy is never touched, and the same code path works in both
primary and secondary workspaces.
- jj-pre-push — the Python tool
that originally inspired this.
jj-hooksadopts its bookmark-update parsing strategy and broadens the runner support. - https://www.aazuspan.dev/blog/automating-pre-push-checks-with-jujutsu/
- Discussion on jj-vcs/jj#405
cargo binstall jj-hooksThis pulls a prebuilt binary from the GitHub Releases page — no compile step.
brew install mattwilkinsonn/tap/jj-hooksjj git clone https://github.com/mattwilkinsonn/jj-hooks
cargo install --path .After install run the interactive setup (optional):
jj-hp initThis prompts to:
- Install a user-level
jj pushalias that delegates tojj-hp push. - Enable
jj-hooks.advance-bookmarksso the local bookmark automatically moves to the fixup commit when hooks autofix something. - Install jjui actions/bindings so
jj-hp pushis reachable from inside jjui.
All three can be reconfigured by running jj-hp init again.
jj-hp completions <shell> emits a clap-generated completion script. The
script wires dynamic completers for --bookmark and --remote that shell
out to jj to enumerate live values.
# zsh: add to ~/.zshrc
eval "$(jj-hp completions zsh)"
# bash: add to ~/.bashrc
eval "$(jj-hp completions bash)"
# fish: write to ~/.config/fish/completions/jj-hp.fish
jj-hp completions fish > ~/.config/fish/completions/jj-hp.fishAfter that, jj-hp push -b <TAB> will complete bookmark names from your repo
and jj-hp push --remote <TAB> will complete remote names.
Note: completion only works for
jj-hpdirectly. Thejj pushalias (installed byjj-hp init) runs through jj's own completion script, which doesn't expand user aliases — sojj push -b <TAB>won't complete bookmark names. Usejj-hp push -b <TAB>instead. This is a limitation of jj's completion script, not jj-hooks.
jj-hp push [-b BOOKMARK]... [--remote REMOTE] [other flags] [-- JJ_GIT_PUSH_ARGS...]
jj-hp run [--stage pre-commit|pre-push] [REVSET]
jj-hp init
jj-hp completions <bash|zsh|fish|powershell>
Global flags:
| Flag | Env | Default | Effect |
|---|---|---|---|
--runner <pre-commit|prek|lefthook|hk> |
JJ_HOOKS_RUNNER |
autodetect | Override runner selection |
--log-level <level> |
JJ_HOOKS_LOG |
warn |
tracing-subscriber filter |
push flags (mirrors jj git push):
| Flag | Default | Effect |
|---|---|---|
-b/--bookmark NAME |
— | Push only this bookmark; repeatable |
-r/--revision REVSET |
— | Push bookmarks pointing at these commits; repeatable |
-c/--change REVSET |
— | Push these commits by creating a bookmark; repeatable |
--remote NAME |
— | The remote to push to |
--all |
off | Push all bookmarks (including new ones) |
--tracked |
off | Push all tracked bookmarks |
--deleted |
off | Push all deleted bookmarks |
--allow-new |
off | Allow pushing new (untracked) bookmarks |
--stage <pre-commit|pre-push> |
pre-push |
Which hook stage to run |
--advance-bookmarks |
from config | Move local bookmarks to fixup commits on autofix |
--dry-run |
off | Forwarded to jj git push |
anything after -- |
— | Forwarded verbatim to jj git push |
run flags:
| Flag | Default | Effect |
|---|---|---|
--stage <pre-commit|pre-push> |
pre-commit |
Which hook stage to run |
positional REVSET |
@ |
Revset to check |
jj-hooks probes the workspace root for these files, in order:
hk.pkl→hklefthook.yml/lefthook.yaml/.lefthook.yml/.lefthook.yaml→lefthook.pre-commit-config.yaml/.pre-commit-config.yml→pre-commit
If multiple match, jj-hooks errors out and asks for --runner. prek is
never autodetected by file — it shares pre-commit's config file. Instead, if
the autodetected runner is pre-commit and prek is on $PATH, jj-hooks
silently uses prek (it's a faster drop-in). Override with --runner pre-commit
to force the slower path.
If no config matches, jj-hp push falls through to plain jj git push.
When hooks modify files in the ephemeral worktree, jj-hooks stages them,
writes a tree, builds a commit with the bookmark's current target as parent,
and anchors that commit under refs/heads/jj-hooks-fixup/<bookmark> just
long enough for jj git import to pick it up. Then it deletes both the
temp jj bookmark and the underlying git ref — the commit itself stays
fully addressable by hash in jj's commit graph.
The output of a push that produced a fixup looks like this:
jj-hooks: Move forward main from abc12345 to def67890: hooks modified files (fixup commit 0123abcd...)
jj-hooks: aborting push
Copy the 0123abcd... and decide what to do with it:
jj log -r 0123abcd # inspect the fixup
jj squash --from 0123abcd --into main # fold the fixes into mainWith --advance-bookmarks (or jj-hooks.advance-bookmarks = true in config),
jj-hooks advances the local bookmark to the fixup commit automatically —
re-run jj-hp push to actually push the fixed version.
The push is always aborted when a fixup commit is created. Run jj-hp push
again after squashing/advancing.
jj-hooks resolves the primary git directory via
.jj/repo/store/git_target, following the .jj/repo pointer file in
secondary workspaces. All git plumbing (worktree creation, commit-tree,
update-ref) targets the primary .git, so commits and refs land in the
shared object database regardless of which workspace you ran from.
All config keys live under jj-hooks.* in jj's user/repo config:
| Key | Type | Default | Notes |
|---|---|---|---|
jj-hooks.advance-bookmarks |
bool | false | Default for --advance-bookmarks |
--runner and --stage are command-line / env only — they belong with the
invocation, not the config.
If you came from
jj-pre-push or just prefer typing
jj push, jj-hp init can wire up an alias for you:
# Added to ~/.config/jj/config.toml by `jj-hp init`
[aliases]
push = ["util", "exec", "--", "jj-hp", "push"]After that, jj push works exactly like jj-hp push. The catch is that
shell completion only sees jj's own completion table, which doesn't expand
user-defined aliases — so jj push -b <TAB> won't complete bookmark names.
For that, fall back to jj-hp push -b <TAB>.
The recommended workflow is to use jj-hp directly. The alias exists for
muscle memory.
just install-deps # install pre-commit, prek, lefthook, hk, markdownlint-cli2, actionlint
just test # check-deps + cargo nextest
just ci # fmt-check + clippy + testThe test suite includes integration tests that build real jj+git repos in tempdirs, install local pre-commit hooks, and run the full push pipeline — including the secondary-workspace path. Every supported runner (pre-commit, prek, lefthook, hk) has dedicated integration tests for pass/fail/autofix.
Apache-2.0.