I switched from Git to Jujutsu (jj) late last year. It interops with Git, so
my collaborators who still use Git haven’t noticed. After I was hardcore into
the Git CLI for 15 years, I didn’t realize how much time I spent fighting it,
even as an expert. The Jujutsu CLI is simpler and fluent. There was a week of
learning curve. I haven’t looked back.
Jujutsu is especially relevant in the age of AI-assisted coding. AI can produce more edits, diffs, branches, commits, and investigations to understand and share with a team. Simon Willison warns software developers against outsourcing all thinking and communication to the AI, and expecting “the ‘code review’ process to handle the rest.”
This is rude, a waste of other people’s time, and is honestly a dereliction of duty as a software developer.
Your job is to deliver code you have proven to work.
That also means clearly communicating what changed and why, to yourself and to teammates.
Understand what the AI changed and give yourself and teammates a coherent,
distinct story they can review, integrate, and maintain. I don’t want to hand
them one giant change, or leave them to decipher a message like
Revert "Revert "hmm this seems broken but it sorta works i dunno"" (quoted
from the seminal article on forensic version control history).
Jujutsu helps me turn exploratory work into smaller, reviewable, rearrangeable
changes.
There are plenty of Jujutsu feature lists and deep dives out there, so I’ll just highlight a few killer features that helped me the last several months, with links for further reading.
Stacked Diffs
I came to Jujutsu for its stacking support. In an async, distributed environment, stacked diffs are table stakes. Having finished conceptual commit #1, it is inefficient to block continuing work on commit #2 until commit #1 passes checks, passes review, and is deemed 100% perfect. Those checks take time, sometimes across timezones. For the author and reader, smaller, self-contained changes are easier to reason about, amend, integrate, and debug. Conversely, combining the concepts from commit #1 and commit #2 into a single large pile is harder to review and increases risk to trunk. More unreviewed corners of the diff unintentionally slip by, and it’s harder to bisect.
jj makes it first class to split, squash, reorder, and rebase
changes into a clean story for code review, while minimizing conflict. Git’s UX
conflates these concepts. Ever notice most of Git’s utility here comes from a
magical text file (initiated by git rebase --interactive) that can perform all
these actions at once, but with little validation, only on the current working
directory, and can lose your commits? Unwinding the stack has to be performed
individually per branch, whose rewrites don’t track each other. Even if the
changes are performed carefully, they lead to repeated, self-inflicted
conflicts. jj gives dedicated UX attention to these operations, simplifying
them, making them safer, and minimizing conflicts (while also making conflicts
first class in history, not just in the working copy).
Let’s say you’re working on a single feature with 5 stacked branches: parser,
parser-api, parser-validation, etc. Each can have any number of commits in
between. Other work can sit alongside the stack, like teammates’ changes to
trunk, or your own exploratory commits. The first branch in your stack has
passed review. It is ready to merge to main. How do you keep the remainder of
the stack in sync?
My team and I tend to squash merge PRs in the GitHub GUI.1 For demonstration,
here’s reproducing that merge in jj. Then, we’ll rebase the whole stack in
one command. Finally, we’ll force push with lease in one command, as we
would with Git.
# Equivalent to squash merge in the GitHub GUI
jj squash --from main..parser --onto main # opens $EDITOR for combined commit message
jj bookmark set main --revision pp # say `pp` is the squash merge commit atop `main`
# Restack the remaining work, including exploratory side branches
jj rebase --revision '(main..parser-api)::' --onto main
jj git pushThat’s it. main..parser-api selects the remaining commits between the newly
merged main and the next bookmark in the stack, parser-api. The trailing
:: adds all those commits’ descendants. Visualized while working on main:
Showing: Before
1. Before
working copy atop main; the stack branches off, with even further side branches.
@ vvswrqzl alice@example.com 2026-07-13 09:00:26 f8c6bbfb│ (empty) (no description set)○ upzkznrn bob@example.com 2026-07-13 09:00:05 main 07608bfd│ Harden CI○ movruppx alice@example.com 2026-07-13 09:00:04 062bec3c│ Update dependencies│ ○ mwtzonnu bob@example.com 2026-07-13 09:00:24 parser-integration 539f299c│ │ Integrate parser│ │ ○ nrroyzxq bob@example.com 2026-07-13 09:00:22 b6a6e7fa│ ├─╯ Try structured errors│ ○ kpopmkrw alice@example.com 2026-07-13 09:00:19 parser-errors a93ebade│ │ Document parser errors│ ○ muoulswl bob@example.com 2026-07-13 09:00:17 parser-validation cfdcce1b│ │ Add validation tests│ ○ nklklurt bob@example.com 2026-07-13 09:00:16 69f5024e│ │ Handle invalid input│ │ ○ pqkwwwts alice@example.com 2026-07-13 09:00:14 b7f79df0│ │ │ Benchmark streaming parser│ │ ○ nswqkxvm alice@example.com 2026-07-13 09:00:13 b03f9d45│ ├─╯ Prototype streaming parser│ ○ nvnqmyyz alice@example.com 2026-07-13 09:00:10 parser-api 3217de9b│ │ Expose parser API│ ○ lzpvzwso alice@example.com 2026-07-13 09:00:08 parser 8b5ae002├─╯ Add parser○ supuptlt alice@example.com 2026-07-13 09:00:02 5f190f5c│ Set up project◆ zzzzzzzz root() 000000002. Squash merged
working copy follows squash merge; the remaining stack not yet rebased.
@ ywqxywyv alice@example.com 2026-07-13 09:00:30 511ed7ac│ (empty) (no description set)○ ppunumnw alice@example.com 2026-07-13 09:00:28 main ca55ec0d│ Add parser○ upzkznrn bob@example.com 2026-07-13 09:00:05 07608bfd│ Harden CI○ movruppx alice@example.com 2026-07-13 09:00:04 062bec3c│ Update dependencies│ ○ mwtzonnu bob@example.com 2026-07-13 09:00:28 parser-integration 2a4670c9│ │ Integrate parser│ │ ○ nrroyzxq bob@example.com 2026-07-13 09:00:28 e87f44b9│ ├─╯ Try structured errors│ ○ kpopmkrw alice@example.com 2026-07-13 09:00:28 parser-errors 6671e6f9│ │ Document parser errors│ ○ muoulswl bob@example.com 2026-07-13 09:00:28 parser-validation 22aeb86f│ │ Add validation tests│ ○ nklklurt bob@example.com 2026-07-13 09:00:28 7c46175e│ │ Handle invalid input│ │ ○ pqkwwwts alice@example.com 2026-07-13 09:00:28 2720282c│ │ │ Benchmark streaming parser│ │ ○ nswqkxvm alice@example.com 2026-07-13 09:00:28 11889091│ ├─╯ Prototype streaming parser│ ○ nvnqmyyz alice@example.com 2026-07-13 09:00:28 parser-api 08db3435├─╯ Expose parser API○ supuptlt alice@example.com 2026-07-13 09:00:02 parser 5f190f5c│ Set up project◆ zzzzzzzz root() 000000003. Restacked
working copy stays atop main as the stack and side branches are rebased.
@ ywqxywyv alice@example.com 2026-07-13 09:00:30 511ed7ac│ (empty) (no description set)│ ○ mwtzonnu bob@example.com 2026-07-13 09:00:32 parser-integration fbe0e86d│ │ Integrate parser│ │ ○ nrroyzxq bob@example.com 2026-07-13 09:00:32 42fcc77b│ ├─╯ Try structured errors│ ○ kpopmkrw alice@example.com 2026-07-13 09:00:32 parser-errors fc3a92a6│ │ Document parser errors│ ○ muoulswl bob@example.com 2026-07-13 09:00:32 parser-validation 841e30ff│ │ Add validation tests│ ○ nklklurt bob@example.com 2026-07-13 09:00:32 8b52049d│ │ Handle invalid input│ │ ○ pqkwwwts alice@example.com 2026-07-13 09:00:32 8a09f88c│ │ │ Benchmark streaming parser│ │ ○ nswqkxvm alice@example.com 2026-07-13 09:00:32 4088962e│ ├─╯ Prototype streaming parser│ ○ nvnqmyyz alice@example.com 2026-07-13 09:00:32 parser-api e8638a0a├─╯ Expose parser API○ ppunumnw alice@example.com 2026-07-13 09:00:28 main ca55ec0d│ Add parser○ upzkznrn bob@example.com 2026-07-13 09:00:05 07608bfd│ Harden CI○ movruppx alice@example.com 2026-07-13 09:00:04 062bec3c│ Update dependencies○ supuptlt alice@example.com 2026-07-13 09:00:02 parser 5f190f5c│ Set up project◆ zzzzzzzz root() 00000000Jujutsu knows how all those stacked descendants relate, and rebases them all in
one go. You can run this from anywhere. If you’ve ever tried this with Git, you
know it is not nearly as simple in the Jujutsu animation above. With Jujutsu,
you don’t have to check out each branch one by one, rebasing each, praying you
turned on Git rerere, resolving conflicts in each, and force pushing each.
Instead, it’s one command to rebase, and one to force push.
Further reading:
Better “Where am I?”
The visual above is the default output of jj log. Let’s take a moment to
appreciate the power of good defaults.
The most frequent query in a CLI is reporting system status, “Where am I?” In
Unix, you type pwd or ls a lot. In Git, it’s git status and git log.
Maybe you have an alias to make those more compact or decorated with metadata
you care about.
git log shows only history reachable from your current head. git log --all
can show other refs, but uncommitted work won’t be viewable at all without
navigating to that worktree’s working directory.
jj log out of the box is vastly more informative. It answers “Where am I?” and
“What else has been worked on recently?” The default shows a compact graph of
your current head change, trunk, tags, and all the open branches you’re tracking
that haven’t been merged, including work you’ve dispatched to worktrees. Notice
the unmerged branches in the jj log visual above, despite my working copy on
main. To remind me what I’m working on and what has been worked on recently,
jj log is more of a one-stop shop, subsuming multiple invocations of
git branch, git log, and external tools like git recent.
Further reading:
Snapshotting
In Git, there’s the staged and unstaged area where it detects whether there’s a working copy diff. Despite there being file changes, they are not saved in version control history. They are not viewable outside Git’s working directory. If you don’t have undo history in your external editor, its history of these uncommitted files can be lost.
In Jujutsu, there’s no such staged or unstaged area. Once you start editing
files, you’re editing a commit. For virtually every jj command you run, behind
the scenes, it snapshots the working copy diff.
This makes spinning up a new idea a breeze. This makes checking out your
teammate’s code without losing your work-in-progress a breeze. Although, as with
Git, you should probably apply a commit message anyway, even if it’s just
TICKET-123: WIP to edit later. This is better than a bunch of anonymous diffs
littered throughout history (or in the case of git stash, out of history).
jj’s automatic snapshotting also rescued me from 2025 Cursor GUI chat panel’s
wonky undo states. In Cursor, it was often unclear what Cmd+Z/u would undo,
and what rewinding checkpoints within the chat thread would do to the actual
working copy. As I’m primarily a terminal user, using it for edit and code
review, jj adds a safety net. Since I am already jj diff-ing whatever the
agent changed, jj is quietly snapshotting repo state. That made jj undo a
decent escape hatch to rewind. Although neither GUI undo states nor jj undo
are substitutes for deliberate commits.
Since my early days with Cursor, I’ve switched to Codex’s desktop app, and it frequently shows just the diff from the latest turn. This is much better than making me re-read the diff of everything it’s done in the chat thread up to that point, wondering what exact subset changed.
The agent doesn’t always get the diff perfect, so I prefer to have it commit
anew and tell me what relevant changeid it worked on. Then I can verify
independently from the specific agent’s UI from any terminal: use the universal
jj diff --revision <changeid>. Or for the whole thread up to that point,
jj diff --revision main..<changeid>.
Then I potentially squash the fixup. Perhaps I squash with no-arg jj squash if
I know my target is the immediate last commit. Or I turn to the handy
jj absorb if the fixup goes further back and it’s obvious to the algorithm
where it goes in history (the 3rd party git absorb is built into jj).
Failing absorb’s automatic detection, there’s the longer, precise, manual form,
jj squash --from <rev2> --to <rev1>. If I’m in a trusting mood with the agent,
I can tell it to perform the precise jj squash for me. If I, jj absorb, or
the agent get anything wrong, I can jj undo.
Worktrees
Worktrees (workspaces in jj parlance) give your repo multiple working copies
on disk, with all history shared between them. For solo dev, worktrees might be
a little overpowered. For keeping your history clear to share with teammates,
however, when your agents are investigating a lot of different ideas, worktrees
can be handy.
Just remember, computers may be good at context switching really fast, but humans aren’t. If you spin up agents on 10 different worktrees, then have to context switch between reviewing 10 different worktrees’ branched commits, you won’t save any time and your critical thinking will be mush.
That said, I usually have my default worktree for me, and up to one other worktree chewing on a background investigation. If the agent isn’t going to stomp on my human changes because it’s in its own worktree, I tell it to commit at will. I can review and clean up its commits later.
The worktree concept isn’t specific to jj, but again jj makes it easy to
rearrange multiple commits. They can be in any worktree. You can view them from
wherever you are with jj log. You can rearrange them from wherever you are.
Even “uncommitted” changes, because all changes are committed via snapshot. All
without needing to change your working directory or working copy.
I don’t need an agent UI-specific worktree option. Just talking to it, telling it to use a worktree for a particular task, with the following minimal instructions from my dotfiles as guardrails and optimization, the agent figures out the rest.
Further reading:
- My worktree instructions, progressively disclosed from the
worktree trigger in
AGENTS.md
Caveats
Again, I’ve used jj pretty seamlessly with my engineering team for several
months. But before you go charging in, it is worth knowing the outstanding,
unsupported Git features. Some points of friction to know
about:
- No Git submodules.
- No Git LFS.
- No Git hooks.
- The tracking issue documents workarounds. And I’m grateful the thread taught me to be careful: Git hooks are perilous.
- So instead of e.g.
pre-commit, my repo AGENTS.md runs e.g.npm run lint:fixat the end of any code-modifying turn (CI runsnpm run lint, as before). That covers more cases than pre-commit hooks anyway.
- Anything that expects
.git/to be present, but there’s only.jj/, such as in ajj workspace.- You’ll need to drop down to
gitand pass the external location of the underlying Git repo via env varsGIT_DIRandGIT_WORK_TREE. Examples:- Repo scripts that enumerate files via
git, e.g.git ls-files. git-crypt.
- Repo scripts that enumerate files via
- You’ll need to drop down to
- Anything that expects Git not on a detached
HEAD.- E.g.
gh pr viewwithout arguments, lists of linked PRs or changed line count. Such tools seem hardcoded togit, no way to swap injj.
- E.g.
- Agents are trained to be fast with
git, and can intuit new tools with familiar concepts likejj, but will incur missteps.- I accept a little extra time taken by the agent, because I get to work on something else, my time is more valuable, and I return to a more followable, rearrangeable history.
Getting Started
In most Git repositories, you can initialize Jujutsu alongside the existing Git history:
cd path/to/a/git/repo
jj git initVoilà. You can do your day-to-day version control with jj. Git remains
underneath for interop and the occasional fallback.
For more exploration tips, again, check out Steve Klabnik’s Jujutsu Tutorial. There will be a week of learning curve. While learning, you can be productive with prior distributed VCS knowhow, but you’ll be a little tripped up by Jujutsu’s new mental model.
For your agent workflow, jj is similar enough to git, you can just talk to
the agent to get it to use jj. The agent can consult the
CLI’s help for anything unexpected.
Note per that article, early in a chat thread or in your AGENTS.md/CLAUDE.md
instructions, you have to yell at Claude in all caps to get it to prefer jj.
Claude does not like to follow instructions otherwise (I haven’t had this
problem with Codex or Cursor). My AGENTS.md doesn’t need too much else, besides
some optional, agent-specific optimizations (e.g. proactively requesting sandbox
escalation, since jj performs writes, its snapshotting, on virtually every
command).
The mainstream VCS du jour usually changes every several years, from e.g. CVS to
SVN, mix some Perforce in there. In the late aughts, the mainstream programming
world solidified on Git and its killer app GitHub. I worried we’d calcify
further around Git, as agents seem to perform better on popular technology
stacks with lots of training data. Thankfully, the Git CLI is not the ultimate
interface, and humans and agents alike pick up jj with ease. Give Jujutsu a
try, and ease your git Stockholm syndrome.
Further reading:
Footnotes
Footnotes
-
You’re making atomic commits, right? If not for your team, for helping yourself think through the evolving code change?
After development and review, this team squash merge policy loses atomic commits, alas. GitHub has no way to enforce semilinear history. The policy is preferable to bad merge bubbles and fixup commits making it to trunk.
Jujutsu distinguishes changes from commits. Because GitHub creates the squash merge outside my local
jjrepository,jjcannot associate the resultingmaincommit with the originalmain..parserchanges. Although a littlejjbenefit is lost in this moment, the code snippet and visualization below showjjhandles the restack anyway. ↩