jj (Jujutsu VCS) Guide
When a .jj/ directory is present, the repository is managed by jj. Use jj commands for all version control operations. Never use raw git commands.
Communication Convention
Use jj commands but speak in git terminology. Since the canonical remote is typically a git repository (GitHub/GitLab), use familiar git terms when communicating:
| jj term | Say this instead |
|---|---|
| change | commit |
| bookmark | branch |
@ (working copy) |
current commit / working copy |
@- (parent) |
parent commit |
| describe | set commit message |
This keeps communication clear for all team members regardless of their jj familiarity.
Critical Safety Rules
Never use raw git commands
In a colocated jj+git repository, raw git commands bypass jj's operation log and can corrupt repository state. The following are forbidden:
git commit— usejj describe+jj newgit add— not needed, jj auto-tracks all changesgit checkout— usejj editorjj newgit branch— usejj bookmarkgit stash— not needed, justjj newgit merge— usejj new commit1 commit2(merge commit)git rebase— usejj rebase
Exception: gh CLI is acceptable (it uses git internally for PR operations).
Always create a new commit before editing pushed code
If the current commit (@) has a branch that has been pushed to the remote, do not edit files directly. This rewrites history and causes force pushes.
Instead:
- Run
jj newto create a fresh empty commit on top - Then make edits
Always pass -m to commands that open an editor
Commands like jj squash, jj split, and jj commit open an interactive editor (vim) to compose a message when no -m flag is provided. Claude Code runs in a non-interactive terminal, so the editor fails with Error reading input, exiting... and the command aborts.
Always pass -m "message" to these commands:
# ✅ GOOD — non-interactive
jj squash --from <rev> --into <rev> -m "combined commit message"
jj squash -m "squash into parent"
# ❌ BAD — opens vim, fails in non-interactive terminal
jj squash --from <rev> --into <rev>
jj squash
Always verify a bookmark exists before pushing
jj git push only pushes bookmarks. If the current commit has no bookmark, nothing gets pushed.
Before pushing:
- Check for a bookmark:
jj log -r @ --no-graph -T 'bookmarks' - If none exists:
jj bookmark set <name> -r @ - Then push:
jj git push --bookmark <name>
Command Reference
| Git operation | jj command | Notes |
|---|---|---|
git status |
jj status |
Shows working copy changes |
git diff |
jj diff |
Diff of working copy |
git show <commit> |
jj diff -r <rev> |
Shows what a specific commit changed |
git diff --staged |
Not needed | No staging area in jj |
git log |
jj log |
Shows change graph |
git log --oneline |
jj log --no-graph |
Compact log output |
git add |
Not needed | jj auto-snapshots all changes |
git commit -m "..." |
jj describe -m "..." then jj new |
Describe current commit, start a new one |
git commit --amend |
jj describe -m "..." |
Message only — file changes are auto-snapshotted (no explicit amend needed for content) |
git branch <name> |
jj bookmark set <name> |
Create/move bookmark |
git branch -d <name> |
jj bookmark delete <name> |
Delete bookmark |
git branch -l |
jj bookmark list |
List bookmarks |
git checkout -b <name> |
jj new + jj bookmark set <name> |
New commit with bookmark |
git checkout <branch> |
jj new <bookmark> |
Creates a new empty commit on top of the branch tip; use jj edit <bookmark> to work directly on an existing commit (will rewrite history if pushed) |
git push |
jj git push |
Push bookmarks to remote |
git push -u origin <name> |
jj git push --bookmark <name> |
Push specific bookmark |
git fetch |
jj git fetch |
Fetch from remote |
git pull |
jj git fetch then jj rebase -d main@origin |
Fetch + rebase onto upstream |
git stash |
jj new |
Previous commit is preserved automatically |
git stash pop |
jj new <previous-change-id> |
Creates a new commit on top of the previous one (unlike git stash pop, this moves you rather than applying changes in place) |
git rebase -i (squash) |
jj squash --from <rev> --into <rev> -m "msg" |
Always pass -m to avoid interactive editor |
git worktree add <path> |
jj workspace add <path> |
Create parallel working copy |
git worktree remove |
jj workspace forget <name> |
Remove workspace |
git worktree list |
jj workspace list |
List workspaces |
Common Workflows
Making changes (the standard workflow)
# 1. Check current state
jj status
jj log
# 2. Make edits (jj auto-tracks everything, no git add needed)
# ... edit files ...
# 3. Set the commit message
jj describe -m "feat: add new feature"
# 4. Create a new empty commit for future work
jj new
Starting a new feature branch
# 1. Make sure we're on the latest main
jj git fetch
jj new main@origin
# 2. Set a bookmark (branch name) for the new commit
jj bookmark set feat/my-feature
# 3. Make edits, describe, push
# ... edit files ...
jj describe -m "feat: implement my feature"
jj git push --bookmark feat/my-feature
Adding commits to an existing PR
# 1. Create a new commit on top (do NOT edit the pushed commit)
jj new
# 2. Make edits
# ... edit files ...
# 3. Describe and push
jj describe -m "fix: address PR feedback"
jj bookmark set feat/my-feature # moves bookmark to new commit
jj git push --bookmark feat/my-feature
Squashing commits
# Squash working copy into its parent (always pass -m)
jj squash -m "feat: combined commit message"
# Squash a specific commit into another
jj squash --from <source-rev> --into <target-rev> -m "combined message"
# Squash all commits on a branch into one
# (identify the range, then squash into the base)
jj squash --from <tip> --into <base> -m "final message"
IMPORTANT: Always pass -m to avoid opening an interactive editor, which fails in non-interactive terminals.
Syncing with upstream
# Fetch latest changes
jj git fetch
# Rebase current work onto updated main
jj rebase -d main@origin
Parallel work with workspaces
# Create a new workspace for parallel work
jj workspace add ../workspace-name
# Work independently in that directory
cd ../workspace-name
jj new main@origin
jj bookmark set feat/other-feature
# ... edit files ...
# Changes are visible across workspaces via jj log
# Clean up when done
cd ../original-dir
jj workspace forget workspace-name
Viewing what changed
# What changed in the working copy
jj diff
# What changed in the parent commit
jj diff -r @-
# Show a specific commit
jj show <change-id>
# Compare two commits
jj diff --from <id1> --to <id2>
Key Differences from Git
- No staging area — all file changes are automatically tracked. There is no
git add. - Commits are mutable —
jj describechanges the message, edits to files are auto-snapshotted. - Working copy is always a commit —
@always refers to a real commit, not uncommitted changes. - Bookmarks are just pointers — they're like git branches but more lightweight. They must be explicitly set.
- Operations are logged — every jj operation is recorded. Use
jj op logto see history andjj op restoreto undo. - Git hooks are bypassed — jj commands do not trigger git-style hooks (pre-commit, pre-push, etc.), even in colocated repos. Quality checks should be handled by Claude Code hooks or CI instead.
When jj Is Not Available
If the .jj/ directory exists but jj is not found in PATH, do not fall back to git commands. This would corrupt jj's internal state. Instead, tell the developer:
"This repository is managed by jj, but jj is not found in PATH. Please install jj or add it to your PATH before performing version control operations."