main

name: jujutsu description: > Always activate FIRST on any git/VCS operations (commit, status, branch, push, etc.), especially when HEAD is detached. If .jj/ exists -> this is a Jujutsu (jj) repo, raw git commands can corrupt data. Essential git safety instructions inside. allowed-tools: Bash(jj *)

Jujutsu (jj) version control system

Jujutsu is a Git-compatible VCS with mutable commits and automatic rebasing.

Tested with jj v0.46.0. Commands may differ in other versions.

Automated and agent environments

When running as an agent:

  • Pass --no-pager to every command that produces output. A pager such as less waits for a keypress and hangs the run. JJ_PAGER overrides ui.pager when you need a jj-specific setting.
jj --no-pager log
jj --no-pager diff
jj --no-pager show <id>
  • Pass messages inline with -m. Commands that open an editor fail without a terminal.
jj desc -m "message"
jj squash -m "message"
  • Confirm mutations with jj st. After squash, abandon, rebase, or restore, check that the operation did what you expected.

  • Avoid the interactive commands. They open a TUI or an editor and hang: jj arrange, jj split, jj resolve, jj diffedit, jj file edit, jj squash -i, and jj absorb -i.

  • Use --no-integrate-operation to run a command without touching the repo state or the working copy, which helps when a background tool might snapshot at the same time.

Core concepts

The working copy is a commit

Your working directory is always a commit, referenced as @. Every jj command snapshots your changes automatically. There is no staging area, and no need to run jj commit.

Commits are mutable

Unlike git, jj commits can be rewritten after you create them. You can change the description, squash changes, rebase, and absorb without making new commits. See “Essential workflow” for the working pattern.

Change IDs vs commit IDs

  • Change ID: a stable identifier such as tqpwlqmp that survives a rewrite. Prefer it when referencing commits.
  • Commit ID: a content hash such as 3ccf7581 that changes with the content.

A change ID can name several hidden or divergent commits at once. Add a numeric suffix to pick one: xyz/0 is the newest version and xyz/1 the one before it. So jj restore --from xyz/1 --to xyz puts xyz back to its previous contents.

Revsets

jj selects commits with a revset language. Common revsets:

  • @, the working copy commit
  • @-, the parent of the working copy
  • ::@, all ancestors of @
  • @::, all descendants of @
  • trunk()..@, the commits between trunk and @
  • bookmarks(), every commit with a bookmark

Useful functions added in recent versions: divergent(), merge_point(), forks(), remote_tags(), diff_lines(), diff_lines_added(), diff_lines_removed(), and builtin_log() (the default jj log revset).

Pass revsets with -r: jj log -r 'trunk()..@'.

String patterns in revsets default to glob matching, so use the substring: or exact: prefix for other kinds. Git-style symbols such as refs/heads/main no longer resolve to revisions; use the bookmark or tag name.

Essential workflow

Starting work: describe first, then code

Write the commit message before the code.

Check that you’re on a blank revision with jj st. If not, start one:

jj new

Then describe the work and make the changes:

jj desc -m "Add user authentication to login endpoint"
# ... edit files ...
jj st

The edits become part of that commit on their own.

Creating atomic commits

Each commit should be one logical change. Message examples:

Add validation to user input forms
Fix null pointer in payment processor
Remove deprecated API endpoints

Viewing history

jj --no-pager log
jj --no-pager log -p
jj --no-pager show <change-id>
jj --no-pager diff --git

jj show takes several revisions at once, and --reversed shows a commit against its parent.

About the jj diff format: the default output is a side-by-side line-number layout (26 26:) that looks nothing like git’s +/- prefixes. It is correct, not stale or corrupt. Use jj diff --git for a standard unified diff.

Moving between commits

jj new
jj new -m "Commit message"
jj edit <change-id>
jj prev -e
jj next -e

Refining commits

Squashing changes

jj squash

jj squash -i opens an interactive UI and hangs in an agent environment.

Splitting commits

jj split is interactive and hangs in an agent environment. To divide a commit, use jj restore to move changes out and create the separate commits by hand.

Absorbing changes

jj absorb

Distributes working-copy changes to the commits that last touched those lines. jj absorb -i and --tool are interactive; skip them in agent runs.

Abandoning commits

jj abandon <change-id>

Descendants are rebased onto the abandoned commit’s parent.

Undoing operations

jj undo
jj redo

These reverse or reapply the last operation, which recovers from a mistaken abandon, squash, or rebase. They refuse to touch an operation from another workspace unless you pass --allow-cross-workspace. To revert one specific operation, use jj op revert; the old jj op undo was removed.

Rebasing commits

jj rebase -d <destination>
jj rebase -r <change-id> -d <destination>
jj rebase -s <change-id> -d <destination>
jj rebase -d main

Restoring files

jj restore
jj restore path/to/file.txt
jj restore --from <change-id> path/to/file.txt

Working with bookmarks

Bookmarks are jj’s branches.

jj bookmark create my-feature -r @
jj bookmark move my-feature --to <change-id>
jj --no-pager bookmark list
jj bookmark delete my-feature

Bookmarks don’t advance when you add commits, so move them by hand before pushing. jj bookmark advance [names] moves bookmarks forward to a target in one step, with --to defaulting to @.

jj bookmark track and untrack take --remote. Without it, every remote bookmark with a matching name is tracked or untracked.

Working with tags

Tags use the same tracking model as bookmarks: fetched tags are tracked by default, and tracked tags are pushed by default.

jj --no-pager tag list
jj tag track <name>
jj tag untrack <name>

jj tag list supports --sort and can be filtered by revset.

Workspaces

A workspace is a working copy plus its repo. One repo can hold several workspaces, each with its own working directory and @, all sharing the same commits, operations, and bookmarks. It is jj’s version of git worktree, handy for running a long build or test while editing elsewhere. Most tasks don’t need one; see the official docs for anything beyond the basics.

Common commands

jj workspace add ../my-tests
jj workspace add --name tests -r <change-id> ../my-tests
jj --no-pager workspace list
jj workspace root [--name <ws>]
jj workspace rename <new-name>
jj workspace forget [<ws>]
jj workspace remove [<ws>]

forget leaves the directory on disk; remove deletes it too, after snapshotting the working copy into a commit. add links by relative path, so a workspace survives being moved together with its parent or run inside a container. list shows each workspace root by default and no longer hides unreachable paths.

In jj log, each workspace’s @ appears as <workspace-name>@.

Colocation

jj workspace add can create a Git worktree alongside the workspace with --colocate/--no-colocate. jj git colocation status, enable, and disable report or toggle this, and they work on child workspaces.

Key semantics

  • Isolation by default. A new workspace gets its own fresh empty commit. Workspaces don’t start out sharing @, and files are never live-mirrored between them.
  • Propagation at command boundaries. Each command snapshots its own files and reads the op log, so it sees commits and bookmarks from other workspaces. There is no filesystem watcher.
  • Stale working copy. If another workspace rewrites this workspace’s @ (via squash, rebase, or abandon), jj refuses commands here until you run jj workspace update-stale. The same recovery applies after an interrupted command.
  • Shared @ is sharp-edged. jj edit <id> lets two workspaces point at the same change without warning. When one mutates it, the other goes stale; if the stale one had unsnapshotted edits, update-stale keeps them as a divergent commit (same change ID, shown as xyz?? in jj log). Resolve the divergence with jj converge, or select one version with the xyz/0, xyz/1 syntax. Avoid sharing @ unless both workspaces are read-only.

Agent guidance

  • Pass --no-pager to jj workspace list.
  • Don’t jj edit a change that another workspace already has as @. That is the usual cause of accidental divergence.
  • Don’t rm -rf a workspace directory without also running jj workspace forget <name>.

Git integration

Working with existing Git repos

jj git clone <url>
jj git init --colocate

jj git init --colocate refuses to run inside a Git worktree and explains the alternatives.

Fetching remote changes

jj git fetch
jj git fetch --remote <remote-name>
jj git fetch -b <branch-name>

jj git fetch reports abandoned commits and forwards git’s stderr, so authentication prompts still reach you. It rebases local descendants onto rewritten parents when the remote preserves change IDs. Tags arrive as <name>@<remote> and are tracked by local tags of the same name. After fetching, rebase your work: jj rebase -d main.

Switching between jj and git (colocated repos only)

This applies only to colocated repos, where .jj/ and .git/ both exist. In a non-colocated repo, do not run git commands; they corrupt jj state.

Switch to git for a merge workflow:

jj st
git checkout <branch-name>

Switch back:

jj edit <change-id>

Git may complain about uncommitted changes when jj’s working copy differs from the git HEAD, so commit your jj work first. jj picks up git’s changes on the next command.

Pushing changes

jj git push -b <bookmark-name>
jj git push -b main

Before pushing, confirm the bookmark points at the right commit (bookmarks don’t auto-advance), the commits are refined and atomic, and the user asked for the push.

jj bookmark move my-feature --to @
jj git push -b my-feature

If no bookmark exists, create one first:

jj bookmark create my-feature
jj git push -b my-feature

jj git push can push to several remotes at once with repeated --remote, allow commits that contain conflicts with --allow-conflicts, forward push options with -o, and auto-track a bookmark given to --bookmark. The default push targets come from revsets.git-push.

Handling conflicts

jj lets you commit conflicts and resolve them later.

jj st

Resolve conflicts by editing the files to remove the markers, then run jj st. Don’t use jj resolve; it is interactive.

Running commands over revisions

jj run runs a command over a set of revisions, each in an isolated working copy, then amends each revision with the result:

jj run -- cargo fix
jj run -j 4 -- cargo test
jj run --ignore-changes -- cargo clippy

It processes revisions oldest to newest by default. --passthrough connects the subprocess to the terminal instead of capturing output, --ignore-changes runs read-only without rewriting commits, and --ignore-errors continues after a failing command.

Resolving divergence

jj converge combines the divergent revisions of a change into one replacement, rebasing descendants and moving bookmarks onto it. It applies heuristics and prompts when they don’t agree. Pass --no-interactive to print a warning and exit instead of prompting, which is what an agent should do.

Inspecting files and other utilities

jj file search -p <pattern>          # grep-like; prints each match with its path
jj file search -p <pattern> -n       # add line numbers; --name-only lists files
jj file delete -r <rev> <path>       # delete files in any revision
jj file show -r <rev> <path>         # print a file from a revision
jj file annotate <path>              # blame
jj util snapshot                     # snapshot the working copy
jj util diff <a> <b>                 # compare two files on disk
jj util backend name                 # show the commit backend
jj util gc                           # prune unreachable data in the store
jj config gc                         # drop config for deleted repos
jj bisect run --range <revs> <cmd>   # find the first bad revision

jj file search matches by regex unless the pattern sets another kind, and it requires -p. jj file edit edits a file in a revision but opens your editor, so it is not for agent runs. jj bisect run checks its endpoints first; pass --trust-endpoints to skip the checks.

Preserving commit quality

Because commits are mutable, refine them before you call the work done.

  1. Review: jj --no-pager show @ or jj --no-pager diff --git.
  2. Keep one logical change per commit.
  3. Write the message as an imperative phrase in sentence case with no full stop: “Add login endpoint”, “Fix null pointer in payment processor”.
  4. Move unrelated changes out with jj restore, then make separate commits.
  5. Put changes in the right commit with jj squash or jj absorb.

Quick reference

ActionCommand
Describe commitjj desc -m "message"
View statusjj st
View logjj --no-pager log
View diffjj --no-pager diff --git
New commitjj new -m "message"
Edit commitjj edit <id>
Squash to parentjj squash
Auto-distributejj absorb
Rebasejj rebase -d <destination>
Abandon commitjj abandon <id>
Undo last operationjj undo
Restore filesjj restore [paths]
Search filesjj file search -p <pattern>
Run over revisionsjj run -- <cmd>
Resolve divergencejj converge --no-interactive
Create bookmarkjj bookmark create <name>
Advance bookmarkjj bookmark advance <name>
Track tagjj tag track <name>
Fetch remotejj git fetch
Push bookmarkjj git push -b <name>
Add workspacejj workspace add <path>
List workspacesjj --no-pager workspace list
Remove workspacejj workspace remove [name]
Forget workspacejj workspace forget [name]
Fix stale working copyjj workspace update-stale

Best practices summary

  1. Describe first: set the message before coding.
  2. One change per commit.
  3. Use change IDs; they survive rewrites.
  4. Refine commits with squash and absorb.
  5. No staging area, no stashing.