Jujutsu for AI-Assisted Coding

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 push

That’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 07608bfdHarden CImovruppx alice@example.com 2026-07-13 09:00:04 062bec3cUpdate 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 parsersupuptlt alice@example.com 2026-07-13 09:00:02 5f190f5cSet up project  zzzzzzzz root() 00000000

2. 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 ca55ec0dAdd parserupzkznrn bob@example.com 2026-07-13 09:00:05 07608bfdHarden CImovruppx alice@example.com 2026-07-13 09:00:04 062bec3cUpdate 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 APIsupuptlt alice@example.com 2026-07-13 09:00:02 parser 5f190f5cSet up project  zzzzzzzz root() 00000000

3. 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 APIppunumnw alice@example.com 2026-07-13 09:00:28 main ca55ec0dAdd parserupzkznrn bob@example.com 2026-07-13 09:00:05 07608bfdHarden CImovruppx alice@example.com 2026-07-13 09:00:04 062bec3cUpdate dependenciessupuptlt alice@example.com 2026-07-13 09:00:02 parser 5f190f5cSet up project  zzzzzzzz root() 00000000
Change IDs remain stable while commit hashes change as the whole graph is rewritten.

Jujutsu 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:

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:fix at the end of any code-modifying turn (CI runs npm 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 a jj workspace.
    • You’ll need to drop down to git and pass the external location of the underlying Git repo via env vars GIT_DIR and GIT_WORK_TREE. Examples:
      • Repo scripts that enumerate files via git, e.g. git ls-files.
      • git-crypt.
  • Anything that expects Git not on a detached HEAD.
    • E.g. gh pr view without arguments, lists of linked PRs or changed line count. Such tools seem hardcoded to git, no way to swap in jj.
  • Agents are trained to be fast with git, and can intuit new tools with familiar concepts like jj, 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 init

Voilà. 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

  1. 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 jj repository, jj cannot associate the resulting main commit with the original main..parser changes. Although a little jj benefit is lost in this moment, the code snippet and visualization below show jj handles the restack anyway.