jj — Jujutsu VCS skill
This hub is jj-native. We do not use GitHub PRs. Reviews happen via compare-URL hand-off to a human:
jj git push --bookmark feature-x
# → https://github.com/<owner>/<repo>/compare/main...feature-x
If you find yourself reaching for gh pr create or "let me install
gh" — stop. That's the wrong mental model for this hub. PR-flavored
work belongs in claude-github-and-spoke.
Seven-point mental model
Adapted from the upstream git comparison doc. Each one is a place git-habits silently mislead.
Working copy is auto-committed. Every
jjcommand snapshots first. There is nojj add, no equivalent of "untracked changes" anxiety. Save the file, run anyjjcommand, and the snapshot is captured.No index.
git add -p; git commitbecomesjj split. To amend the parent with current changes:jj squash(orjj squash <file>for a single path,jj squash -ifor an interactive picker).No current bookmark — this is the biggest git-habit trap.
jj describeandjj newdo not advance any bookmark. Bookmarks (jj's term for branches) must be explicitly moved withjj bookmark create <name> -r @(new) orjj bookmark set <name> -r @(move existing) beforejj git push. The-r @(working copy) vs-r @-(parent) choice trips git brains; for "the change I just described and want to push",-r @is correct.Conflicts commit.
jj rebase/jj mergenever fail on conflict; conflicts get recorded into the commit and resolved later withjj resolve. Don't treat exit code 0 as "merge succeeded" — checkjj logfor aconflictmarker on the commit.Descendants auto-rebase.
jj rebase -r X -d Ypropagates to X's children automatically. No manual fix-up of stacked commits.Bookmarks travel by name across remotes.
jj git push --bookmark mainis the whole story; noorigin main:mainceremony, no upstream-tracking dance.jj op log+jj op restore <id>undoes anything. Replacesgit reflog. Every jj operation (includinggit push,bookmark create, rebase, even pulls) gets a unique op ID. To roll back:jj op log # find the op to restore to jj op restore <op-id-prefix> # restore that stateThis is the agent-recovery primitive that earns jj its keep. If something goes sideways, the path back is always one command away.
Common verbs (cheatsheet)
# Reading state
jj log # default-command, configured at boot
jj log -r 'all()' # everything, including remote bookmarks
jj show # working copy diff vs. parent
jj diff -r @ # alias
jj op log # operation history
jj bookmark list -a # local + remote bookmarks
# Making changes
jj new <rev> # start a new change on top of <rev>
jj describe -m "msg" # set the current change's message
jj squash # fold @ into @- (amend parent)
jj squash --from <rev> --into <rev> # arbitrary move
jj split # interactively split @ into multiple commits
jj abandon # drop @, move cursor to parent
# Bookmarks (= branches)
jj bookmark create feature-x -r @ # new bookmark at working copy
jj bookmark set feature-x -r @ # move existing
jj bookmark delete feature-x # locally
jj bookmark forget feature-x # also forget remote tracking
# Talking to git remotes
jj git clone https://github.com/owner/repo .spokes/repo
jj git fetch # update all remote bookmarks
jj git push --bookmark feature-x # push one bookmark
jj git push # push all locally-modified tracked bookmarks
jj git push --deleted # push all locally-deleted bookmarks
# (separate form — NOT --bookmark + --deleted)
# Rebasing / reorganizing
jj rebase -r X -d Y # move X (and descendants) onto Y
jj rebase -s X -d Y # move X and its subtree onto Y
jj resolve # work through conflicted files
# Undo
jj op log
jj op restore <id-prefix>
jj undo # alias for restoring the previous op
End-to-end recipes
Clone and ship a change
jj git clone https://github.com/owner/repo .spokes/repo
cd .spokes/repo
jj new main # change on main
$EDITOR src/thing.py
jj describe -m "fix: handle empty input in thing()"
jj bookmark create feature-x -r @
jj git push --bookmark feature-x
echo "Review: https://github.com/owner/repo/compare/main...feature-x"
Stack a second change on top
jj new # child of current @ (= feature-x tip)
$EDITOR src/other.py
jj describe -m "test: cover the empty-input path"
jj bookmark set feature-x -r @ # advance the bookmark
jj git push --bookmark feature-x
Split a commit you already made
jj log -r feature-x # find the commit to split
jj edit <commit-id> # check it out as working copy
jj split # interactive: stage chunks into commit 1, rest into commit 2
jj bookmark set feature-x -r @ # bookmark moves to the second piece
Squash a fix into the previous commit
$EDITOR src/thing.py # add the fix
jj squash # fold @ into @-
# (or `jj squash --into <id>` for any ancestor)
Resolve a conflict
jj rebase -r feature-x -d main # may record a conflict
jj log # check for `conflict` marker
jj resolve # opens conflicted files in $EDITOR
jj squash # if you want the resolution folded into the original commit
Undo the last push
jj op log --limit 5 # find the push op
jj op restore <op-id-prefix-of-the-one-BEFORE-the-push>
# Note: this restores LOCAL state. The remote bookmark still exists.
# To remove the remote: `jj bookmark delete <name>; jj git push --deleted`
# or `git push origin --delete <name>` (colocated repos support both).
Deleting a remote bookmark
jj git push --deleted is its own form — not a flag combined with
--bookmark. Order matters:
jj bookmark delete feature-x # mark deleted locally
jj git push --deleted # ship all locally-deleted bookmarks
Since the spoke is colocated, plain git also works as a fallback:
git push origin --delete feature-x
When jj doesn't cover something — pointers
- Git command table — side-by-side translation
- Working with GitHub — GitHub-specific patterns (force pushes, PR handoff)
- Bookmarks concept — full bookmark semantics
- Operation log — recovery and history rewriting
- Revsets
— the language for selecting revisions in
-rflags
When to stop and ask
- A
jj git pushwould touch a repo the user doesn't own and you have no clear instruction to act there. The compare URL is a soft commit; the bookmark on the remote is harder to retract. jj op logshows operations from before this session that you don't recognize — surface them before continuing.- A rebase records conflicts you can't tell how to resolve from the
surrounding context — show
jj resolve --listoutput and ask. - You're about to run
jj abandonon a commit that has descendants on bookmarks you didn't create — confirm first.
Gotchas (diagnosed during this hub's bring-up)
jj git push --bookmark X --deletedis rejected by jj's argument parser. Use the two-stepjj bookmark delete X; jj git push --deleted.--colocateonjj git cloneis now the default in v0.41+; the flag still exists for compat but has no effect. Don't sweat including it.- The auto-snapshot means an unsaved-buffer mistake gets captured. If
you snapshot bad content into
@, the fix is either edit-and-resnapshot orjj abandon+jj new.