How this skill is triggered — by the user, by Claude, or both
Slash command
/jj-workflow:jj-conceptsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This project uses **jujutsu (jj)** instead of git. jj is a Git-compatible VCS with a different mental model. Always use `jj` commands, never `git`.
This project uses jujutsu (jj) instead of git. jj is a Git-compatible VCS with a different mental model. Always use jj commands, never git.
At the start of any jj session, run:
jj config list
This reveals user-defined aliases, custom revset aliases, preferred diff tools, and default commands that change how jj behaves. Key things to look for:
aliases.* — user shortcuts (e.g. jj l, jj tug). jj tug is a common community alias that moves the closest bookmark to @-. These may approximate newer built-ins — prefer built-in commands when both exist (e.g. jj bookmark advance supersedes jj tug).ui.default-command — what jj with no args shows. The value ["log", "-r", "(main..@):: | (main..@)-"] (commits between main and @, plus siblings) is what jj itself suggests in its output, so it's common.ui.diff-formatter — may use an external tool like difft instead of built-inui.paginate — if "never", the --no-pager flag is redundantjj is Git-compatible: a repo can be colocated, meaning .jj/ and .git/ live in the same directory and operate on the same commits. This is the normal setup when adopting jj on an existing project.
jj git init --colocate in an existing git repo (or jj git clone of a git remote). Without --colocate, jj keeps its own internal git store and there is no top-level .git/..git/ directory and git tooling (CI, hooks, editors that read git state). That does not mean you should drive with git — the block-git.sh hook still routes you to jj. Git just observes the state jj writes.jj ships authoritative docs for its major subsystems. Prefer these over training-data recall when the question is about syntax or semantics:
jj help <subcommand> # Per-command flags and usage
jj help -k <topic> # Long-form documentation by keyword
Available -k topics and when to consult each:
| Topic | When to read |
|---|---|
tutorial | First exposure to jj concepts; conflict workflow walkthrough |
revsets | Building any non-trivial -r expression beyond @, @-, trunk() |
templates | Customizing jj log / jj show output formatting (-T) |
filesets | Path/glob expressions used with file-aware commands |
bookmarks | Local vs remote tracking, push semantics, divergence |
glossary | Resolving unfamiliar terminology (e.g. "abandoned", "divergent", "hidden") |
config | Settings keys, config file locations, scopes |
Run the relevant jj help -k topic when in doubt rather than guessing at flags or syntax.
jj's vocabulary is deliberate. Use it precisely both in commands and when talking to the user — sloppy terminology produces sloppy commands.
jj log) until explicitly abandoned. This is a normal, named concept in jj — not a warning sign.Practical consequences:
jj bookmark create / jj bookmark set, and use the word "bookmark" in the response.branch here is a topology, and jj rebase -b <rev> -o main handles it.jj branch … in a command. There is no such subcommand; the verb is jj bookmark. A user who types jj branch is reaching for git muscle memory.@ is not "on" a bookmark. New commits do not advance any bookmark. You move bookmarks explicitly with jj bookmark set / move / advance, or implicitly via jj git push after a rewrite (rewrites are followed by change ID)..git/), each git branch corresponds to a jj bookmark of the same name. That's an implementation bridge, not a conceptual equivalence — keep using "bookmark" when describing user-facing operations.@ is a real commit, not a staging area. Every file edit automatically amends @. No git add needed — just edit files.
Every commit has two identifiers: a change ID (e.g. kzomqsrt) that stays the same across rewrites, and a commit ID (hash) that changes. Always use change IDs when targeting specific commits — they survive rebases, amends, and squashes. The tutorial says: "We will generally prefer change IDs because they stay the same when the commit is rewritten."
jj edit <change-id> makes any past commit the working copy. Descendants auto-rebase. You can fix a bug five commits ago without stashing, branching, or cherry-picking.
This is a core jj workflow, so use it deliberately:
jj edit <rev> moves @ onto that revision. There is no "detached HEAD" warning and nothing to stash — @ simply is that commit now.<rev> in place (its change ID is preserved; it gets a new commit ID).git rebase --continue dance; if a descendant can't apply cleanly, the conflict is stored in that commit and you resolve it whenever you like — your current edit is never blocked.@ wherever you want next (jj edit <other>, or jj new <tip> to get back on top). The edits you made stay with the revision you made them on — they don't follow the working copy.Contrast with jj new <rev>, which creates a new child commit on top of <rev> rather than editing <rev> itself. Reach for jj edit to amend an existing commit; jj new to start fresh work.
Conflicts are stored inside commits. A rebase that produces conflicts still succeeds — the conflicted state is committed and you resolve it later. jj log marks conflicted commits with ×.
There is no true "amend" in jj. Every rewrite (describe, squash, rebase) creates a new commit ID while preserving the change ID. jj prevents rewriting commits that are in the immutable set (typically anything merged to main/trunk()). Use --ignore-immutable as a last resort escape hatch, but prefer rebasing on top instead.
Every jj command is recorded. jj undo reverts the last operation — not just the last commit, but any operation including rebases, squashes, and pushes. Much simpler than git reset variants.
jj show # Commit metadata + full diff — prefer over status+diff (one command)
jj status # Changed filenames only (use when diff would be too large)
jj diff # Full diff without commit metadata
jj show <change-id> # Show a specific commit
jj log # Recent commits (graph view)
jj log -r 'all()' # All commits
jj log -r '..@' # Everything up to working copy
jj evolog # How the current change evolved over time
jj new -m "msg" # New empty commit with description (no editor)
jj new <change-id> -m "msg" # New commit on top of a specific revision
jj commit -m "msg" # Finalize @ with message, create new empty @
jj commit -m "msg" f1 f2 # Commit only specific files
jj describe -m "msg" # Set/change description of @ without finalizing
jj edit <change-id> # Make any commit the working copy (descendants auto-rebase)
jj abandon <change-id> # Drop a commit; descendants rebase to its parent
Always use -m "..." with any commit-like command — without it, an editor opens.
jj squash -m "msg" # Move all of @ into parent
jj squash file1 file2 -m "msg" # Move specific files into parent
jj squash --from <id> --into <id> -m "msg" # Move changes between any two commits
jj squash --into <ancestor> file1 -m "msg" # Targeted fixup: send specific files to a known ancestor
jj squash -u # Use destination's message (no editor)
jj split file1 file2 -m "msg" # Split @: listed files → first commit, rest stay
jj split -r <id> file1 file2 -m "msg" # Split an arbitrary commit (not just @)
Use jj squash --into <ancestor> when you know exactly which commit a change belongs to. When you don't — "fold these working-copy fixes back into wherever each line came from" — use jj absorb instead (see below), which routes each hunk automatically.
jj squash and jj split open an editor by default (when descriptions need to be combined, or when no filesets are given). Avoid this by always providing filesets and -m.
jj diffedit always opens an interactive diff editor — avoid it. Instead:
jj restore <files> --from <id>jj squash <files>jj restore is also useful for reverting a file to its state in a specific revision without creating a new commit — the change lands in @ as an uncommitted edit:
jj restore <file> # Restore file to parent's state (discard edits)
jj restore <file> --from <change-id> # Restore file to any revision's state
This is preferable to manually editing a file back to a known state.
jj rebase takes one of three source selectors plus a destination. They are not interchangeable:
| Flag | Moves | Mnemonic |
|---|---|---|
-r <rev> | just that one commit | "this revision only" — descendants stay put, get rebased onto its old parent |
-s <rev> | <rev> and all descendants | "source subtree" |
-b <rev> | the whole branch containing <rev> that isn't yet in the destination | "branch" |
Destinations:
| Flag | Meaning |
|---|---|
--onto/-o <rev> | New parent (most common) |
-A <rev> | Insert after <rev> (between it and its current children) |
-B <rev> | Insert before <rev> |
--onto/-o is the current name; -d/--destination still works as a legacy alias but jj help rebase no longer advertises it — prefer -o.
jj rebase -s <feature> -o main # Move feature stack onto main
jj rebase -r <fix> -A <target> # Reorder: move <fix> to sit after <target>
jj rebase -b @ -o main@origin # Update local stack onto fetched main
To reorder commits, prefer the explicit non-interactive form (jj rebase -r C --before B) over jj arrange, which opens an interactive TUI.
Rebase never blocks on conflicts — see Resolving Conflicts below.
These are first-class jj commands that replace multi-step git workflows. Reach for these before composing the same effect from jj rebase + jj squash + jj new.
jj absorb — distributes the working-copy changes into the ancestor commits that last modified the same lines. Replaces git commit --fixup + git rebase -i --autosquash in one command. Targets mutable() by default; restrict with --into <revset> or [filesets...].jj fix — runs configured formatters ([fix.tools]) on the changed files of one or more revisions, then propagates results to descendants without producing conflicts. Replaces format && git commit --amend && rebase.jj duplicate -r <rev> [-A|-B|-o <dest>] — copies a commit's content as a new change. The canonical "cherry-pick" replacement. (-o/--onto; -d is still an accepted alias.)jj revert -r <rev> -o <dest> — applies the inverse of <rev> at <dest>. (The old jj backout no longer exists.) -A/-B insertion flags also work; -d is an alias of -o.jj interdiff --from <a> --to <b> — diffs the diffs of two revisions. Use it to see what changed between two iterations of the same change (e.g. before vs after a force-push: jj interdiff --from push-xyz@origin --to push-xyz). Not the same as jj diff --from --to, which compares file contents.jj metaedit -r <rev> — change author/email/timestamp without touching content. Use --update-change-id to detach a copy from the original change ID.jj parallelize <revset> — turn a linear chain into siblings sharing the same parent. Inverse: jj rebase them back into a line.jj simplify-parents -r <rev> — remove redundant parents from a merge commit when one parent is an ancestor of another.jj file <subcmd> — list, show <file>, annotate <file> (blame), track <pat>, untrack <pat>, chmod {n|x} <file>. Prefer these over shelling out to cat/chmod when you want the working-copy snapshot semantics.Conflicts in jj are stored inside commits, so jj rebase always succeeds. The conflicted commit gets an × marker in jj log.
jj resolve --list # List conflicted files
jj resolve --tool :ours # Non-interactive: accept side 1
jj resolve --tool :theirs # Non-interactive: accept side 2
For custom resolution: edit the conflict markers directly in the file. jj auto-detects the resolution on the next command — no need to run jj resolve at all.
Standard workflow:
jj new <conflicted-change-id> # Create a child of the conflicted commit
# edit the file to resolve conflicts
jj squash -m "resolve conflict in <file>" # Squash resolution into the conflicted commit
jj's conflict markers are not git's. They include a %%%%%%% diff block showing how one side evolved relative to the merge base, and a +++++++ block holding the literal content of the other side. See references/conflicts.md for the marker format and worked examples before editing a jj conflict by hand.
Reach for the simplest tool that fixes the problem. Treat the jj op family — especially jj op restore — as a last resort, not a first move. The op log rewinds whole-repo state and can silently discard unrelated work that happened after the operation you're targeting. Before touching it, ask whether a narrower command does the job:
| What you want | Use this first | Not this |
|---|---|---|
| Take back the operation you just ran | jj undo (repeat for more) | jj op restore |
| Re-apply something you just undid | jj redo | jj op restore |
| Cancel the effect of a committed change, keeping history | jj revert -r <rev> -o @ | jj op revert |
| Throw away working-copy edits / reset a file | jj restore [<files>] | jj op restore |
Only when none of those express what you need — e.g. you must surgically undo one operation several steps back while keeping everything after it, or recover from a tangle that simple undo can't reach — drop down to the op log:
jj op log # List all operations
jj undo # Undo the last operation (any operation, not just commits) — START HERE
jj redo # Redo the last undone operation
jj op show <op-id> # Inspect what a specific operation changed
jj op revert <op-id> # Surgical: undo one specific past operation, keeping later ones
jj op restore <op-id> # Blunt + destructive: discard ALL later operations to return to a past state
jj --at-operation <op-id> <cmd> # Run any read-only command against a past repo state
jj undo is the answer to almost any recent mistake — much simpler than git reset --hard, git reflog, etc. Between the two op-log rewrites, prefer jj op revert (surgical, keeps later work) over jj op restore (discards everything after the target). --at-operation (alias --at-op) is the read-only time machine — safe to use freely, since it only inspects a past repo state without modifying anything.
Bookmarks map to git branches when pushing, but work differently:
Key differences from git branches:
@ is not "on" a bookmark. Bookmarks are just labels pointing at commits, not a current location.jj commit your new work sits above the bookmark, so jj git push pushes nothing new. You must first advance the bookmark — jj bookmark move <name> --to @- (or jj bookmark advance / jj ba) — then push. The /jj-commit command ends with this reminder for exactly this reason.bookmark@origin is a separate ref. After fetching, you can see local and remote diverge before pushing.jj bookmark move name --allow-backwards (unlike git branch -f).jj bookmark create <name> -r <change-id> # Create at specific commit
jj bookmark set <name> # Point bookmark to @
jj bookmark move <name> --to <change-id> # Move to any commit (only existing bookmarks)
jj bookmark move <name> --to <id> --allow-backwards # Move to an ancestor
jj bookmark advance # Advance closest bookmark(s) to @
jj bookmark advance <name> # Advance specific bookmark to @
jj bookmark advance <name> --to <change-id> # Advance specific bookmark to target
jj bookmark list --all # List local + remote tracking bookmarks
jj bookmark delete <name> # Mark deleted locally
jj git push --bookmark <name> # Push a specific bookmark
jj git push --deleted # Propagate local deletions to remote
jj git push # Push all changed bookmarks
jj git fetch # Fetch from remote
No --force needed after rewrites. jj git push handles rewritten history transparently — it reports "Move sideways bookmark" and force-pushes implicitly. Unlike git, there is no --force-with-lease muscle memory to invoke. jj tracks change IDs separately from commit IDs, so it knows the rewrite was intentional.
bookmark move vs bookmark advance:
move — explicit: you say exactly where the bookmark goesadvance — without args, finds the closest bookmark(s) that are ancestors of @ and moves them forward to @; with a name, advances that specific bookmark to @; useful after squashing/rebasing when the bookmark is lagging behind your workBookmark operations do not affect @ — unlike git checkout <branch> which moves HEAD and updates the working tree, moving or advancing a bookmark in jj is purely administrative. The working copy stays exactly where it is.
A divergent change is one change ID that resolves to two or more visible commits — jj log marks them (divergent) and jj log -r <change-id> errors with "Change ID is divergent" (disambiguate with <change-id>/0, /1, …).
The most common way to hit this: you push a feature commit to a branch, the PR is merged (squash/rebase merge) so the same change lands on main as a new commit ID, then jj git fetch imports it. Now your local pushed commit and the merged-into-main commit share one change ID → divergent. The merged commit on main is the keeper; your local copy is a stale duplicate.
Fix — usually just fetch again:
jj git fetch # prunes the (now-deleted) merged push branch; jj auto-abandons the unreachable duplicate
jj log # confirm the change is no longer marked (divergent)
jj new main # start fresh work on top of the merged main, if you aren't already there
GitHub typically deletes the PR branch on merge, so a fetch prunes the remote-tracking ref and the duplicate becomes unreachable and is abandoned automatically.
If a fetch doesn't clear it (the remote branch still exists, or the ref lingers as name@origin): the duplicate is held alive — and marked immutable — by that remote-tracking ref, so jj abandon will refuse it until the ref is gone. Drop the ref, then abandon:
jj bookmark forget --include-remotes <push-branch> # remove local + remote-tracking refs (no remote push)
jj abandon <commit-id> # commit-id, not change-id (change-id is ambiguous while divergent)
If the branch genuinely still exists on the remote and you want it gone everywhere, delete it on the remote (e.g. via the PR/host UI) or jj bookmark delete <push-branch> then jj git push --deleted. Everything here is recorded in the op log — jj undo backs out a wrong step.
jj workspaces are like git worktrees but better:
@ (working copy)jj workspace add <name> --revision @jj workspace forget <name>| Symbol | Meaning |
|---|---|
@ | Current working copy commit |
@- | Parent of working copy |
trunk() | Main branch tip (remote main/master) |
mine() | Commits authored by you |
bookmarks() | All bookmarked commits |
foo- | Parent of foo |
foo+ | Children of foo |
::foo | Ancestors of foo |
foo::bar | DAG range |
foo..bar | Range (like git's) |
Prefer change IDs over relative refs like @- when targeting specific commits — change IDs are stable across rewrites, relative refs shift as the working copy moves.
references/git-to-jj.md — comprehensive git-to-jj command mappingreferences/conflicts.md — jj's conflict marker format and resolution recipesreferences/revsets.md — curated revset language: operators, common functions, idiomsreferences/filesets.md — curated fileset language for file-aware commandsjj help -k <topic> — authoritative docs for revsets, templates, filesets, bookmarks, config, glossary, tutorialnpx claudepluginhub kalupa/jj-workflow --plugin jj-workflowCreates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.