Welcome back, fellow Jujutsu explorers! You’ve mastered the core concepts of jj, from its unique working-copy-as-a-commit model to navigating mutable history and leveraging the powerful operation log. Now, it’s time to truly make jj your own.

In this chapter, we’ll dive deep into customizing jj to fit your personal workflow like a glove. We’ll explore configuration files, create powerful aliases for common commands, integrate jj with your favorite editors and diff tools, and even craft custom output templates. The goal is simple: to make your jj experience as efficient, intuitive, and productive as possible.

Think of this as equipping your jj toolkit with personalized shortcuts and powerful automation, transforming it from a robust VCS into an indispensable daily companion. Let’s get started!

The Heart of Customization: jj Configuration Files

Just like many powerful command-line tools, jj uses configuration files to let you tailor its behavior. These files are written in the TOML format, a simple, human-readable data serialization language.

What are jj Configuration Files?

jj configuration files are plain text files that store settings and preferences for how jj should operate. They allow you to define everything from how commit messages are formatted to which external tools jj should use for diffing or merging.

Why do they exist? Configuration files exist to provide flexibility and consistency. Instead of typing out long command-line arguments every time, you can set defaults that jj automatically applies. This saves time, reduces errors, and ensures your preferred workflow is always active.

What problem do they solve? They solve the problem of repetitive command-line input and the need to adapt jj’s default behavior to individual preferences or project requirements. Without them, jj would be less adaptable and more cumbersome for daily use.

Configuration Lookup Hierarchy

jj looks for configuration in a specific, hierarchical order. This allows you to set global defaults and then override them for specific repositories:

  1. System-wide configuration: (Least common for individual users)
  2. User-specific configuration: ~/.jjconfig.toml (or %USERPROFILE%\.jjconfig.toml on Windows). This is your primary file for global settings, aliases, and tool preferences that apply to all jj repositories.
  3. Repository-specific configuration: .jj/repo/config.toml inside a jj repository. Settings here will override anything in your user-specific file for that particular repository, allowing project-specific customizations.

This hierarchical approach ensures you can have consistent global settings but fine-tune behavior for specific projects without affecting others.

flowchart TD A[Start jj Command] --> B{Repository Config Present}; B -->|Yes| C[Read Repo Config]; B -->|No| D{User Config Present}; C --> D; D -->|Yes| E[Read User Config]; D -->|No| F[Use Default Settings]; E --> F; F --> G[Apply Final Configuration];

Explanation of the flow:

  • When you run a jj command, it first checks if you’re inside a jj repository.
  • If a repository-specific .jj/repo/config.toml exists, jj reads those settings first.
  • Next, it reads your global ~/.jjconfig.toml file.
  • Any settings found in ~/.jjconfig.toml will be overridden by identical settings found earlier in .jj/repo/config.toml.
  • Finally, any settings not specified in either file will fall back to jj’s built-in default values.

Step-by-Step Implementation: Setting Up Your Global ~/.jjconfig.toml

The ~/.jjconfig.toml file is where most of your personal customization will live. If it doesn’t exist, jj will simply use its default settings. Let’s create one and add a common preference.

First, open your terminal.

# Create an empty config file if it doesn't exist
touch ~/.jjconfig.toml

# Open it with your favorite text editor (e.g., nano, vim, code)
# For example, using nano:
nano ~/.jjconfig.toml

Now, let’s add a basic configuration that makes jj’s output more informative: always show the commit message when running jj log or jj status.

Add the following to your ~/.jjconfig.toml file:

# ~/.jjconfig.toml
# Configuration for Jujutsu (jj) VCS
# Last updated: 2026-05-19

[ui]
# Always show the commit message in `jj log` and `jj status` output.
log-template = "commit_id: {commit_id}\nauthor: {author}\ndate: {time}\n\n{description}"

How it functions:

  • [ui] declares a section for User Interface related settings. Sections help organize your configuration.
  • log-template is a string that defines how jj should format the output of commands like jj log and jj status.
  • {commit_id}, {author}, {time}, and {description} are placeholders. jj replaces these with the actual data from each commit. For example, {commit_id} becomes the full commit hash, and {description} becomes the commit message. The \n characters create new lines in the output.

Save and close the file. Now, when you run jj log or jj status in a jj repository, you’ll see the commit message included automatically!

# Try it out in a jj repo (make sure you have some commits)
# If you don't have a jj repo, create one:
# mkdir my_jj_repo && cd my_jj_repo && jj init
# echo "Hello Jujutsu" > README.md && jj commit -m "Initial commit"
# echo "Another line" >> README.md && jj commit -m "Add another line"

jj log -r @
jj status

You should now see more verbose output, including the commit message, directly in jj log and jj status.

Boosting Efficiency with Aliases

Aliases are short, custom commands that expand into longer, more complex jj commands. They are a massive productivity booster, letting you type less and achieve more. If you find yourself typing a repetitive jj command sequence, it’s a perfect candidate for an alias.

What are jj Aliases?

jj aliases are custom shortcuts you define for jj commands. They allow you to assign a short, memorable name (like st for status) to a full jj command, including its arguments and options.

Why do they exist? Aliases exist to streamline your workflow and reduce cognitive load. They make frequently used commands quicker to execute and help bridge the gap for users migrating from other VCS tools (like Git) who are used to certain command shortcuts.

What problem do they solve? They solve the problem of typing long or complex commands repeatedly. They also allow you to create custom workflows by chaining multiple commands or applying specific revsets and templates with a single, simple command.

Step-by-Step Implementation: Defining Aliases in ~/.jjconfig.toml

Aliases are defined in the [aliases] section of your ~/.jjconfig.toml (or repository config).

Let’s add some common aliases to our ~/.jjconfig.toml file. Open it again:

nano ~/.jjconfig.toml

Add the following lines to the end of your file:

# ~/.jjconfig.toml
# ... existing ui section ...

[aliases]
# 'jj st' for 'jj status' - a common Git alias habit
st = "status"

# 'jj co' for 'jj checkout' - another Git habit, now for 'jj edit'
# In jj, 'checkout' is similar to 'edit' for moving the working copy
co = "edit"

# 'jj up' for 'jj update' - frequently used to move the working copy
up = "update"

# 'jj prev' to go to the parent of the current working-copy commit
# '@-' is the revset for the parent of the working copy
prev = "edit @-"

# 'jj next' to go to the child of the current working-copy commit
# '@+' is the revset for the child of the working copy
next = "edit @+"

# 'jj tree' to show the commit graph with a simplified template
# This combines 'log' with a custom template for a tree-like view
tree = "log -T 'commit_id.short} {branches} {description.first_line() | indent(\"  \")}'"

How it functions:

  • [aliases] marks the start of the aliases section in the TOML file.
  • Each line under [aliases] defines an alias using the format: alias_name = "full_command_string".
  • When you type jj alias_name, jj replaces alias_name with full_command_string and executes it.
  • Notice how the tree alias encapsulates a log command with a revset (-T '...'), demonstrating that aliases can indeed wrap complex command arguments and templates.

Save and close the file. Now you can use these shorter commands!

Practical Alias Examples

Let’s see these aliases in action. Navigate to a jj repository.

# Instead of 'jj status', you can now type:
jj st

# Instead of 'jj edit <commit_id>', you can use 'jj co' to edit the working copy
# For example, to edit the current commit:
jj co @

# Instead of 'jj update @', you can now type:
jj up

# To go to the parent commit of your current working copy:
jj prev

# To go to the first child commit of your current working copy:
jj next

# To view a simplified commit graph with our custom template:
jj tree

These small changes add up to significant time savings and a more fluid workflow.

Mini-Challenge: Create Your Own Alias

Challenge: Imagine you frequently need to view the current working-copy commit and all its immediate children, but only showing the short commit ID, the author, and the first line of the description. Create an alias called jj children-log that runs jj log -r '@ | children(@)' -T 'commit_id.short} {author.name}: {description.first_line()}'.

Hint: Add a new entry under the [aliases] section in your ~/.jjconfig.toml file. Remember the structure alias_name = "full_command_string".

What to observe/learn: How easy it is to extend jj’s functionality with simple aliases, and how revsets (like @ | children(@)) become even more powerful and accessible when combined with templates and aliased.

Integrating Your Favorite Tools: Editor and Diff Configuration

jj plays nicely with external tools. You can configure it to use your preferred text editor for commit messages and your favorite graphical diff tool for reviewing changes. This integration is key to a seamless development experience.

Configuring Your Editor for Commit Messages

By default, jj will use your system’s EDITOR environment variable. If that’s not set or you want to specify a different editor specifically for jj, you can configure it in ~/.jjconfig.toml.

Let’s set jj to use code --wait for VS Code, which is a popular choice. The --wait flag is crucial as it tells jj to wait for the editor to close the file before jj continues its operation.

Open ~/.jjconfig.toml again:

nano ~/.jjconfig.toml

Add the following to your [ui] section:

# ~/.jjconfig.toml
# ... existing sections ...

[ui]
# ... log-template ...
editor = "code --wait" # Use VS Code for commit messages and other edits

How it functions:

  • editor = "code --wait" tells jj to launch the code executable with the --wait flag whenever it needs to open a file for editing. This happens, for example, when you run jj commit without a -m message, or jj describe to edit a commit’s description.
  • Important: Replace "code --wait" with the command for your preferred editor and its “wait” flag.
    • "nvim --listen" for Neovim
    • "subl --wait" for Sublime Text
    • "atom --wait" for Atom
    • "nano" or "vim" if you prefer terminal editors (they inherently wait).

Save and close. Now, try creating a commit without a message:

# Make a change, then commit without a message
echo "a new feature line" >> new_feature.txt
jj commit

Your configured editor should open with a temporary file for your commit message! Type your message, save, and close the editor. jj will then finalize the commit.

Configuring External Diff Tools

jj’s built-in diff is functional, but often a graphical diff tool provides a much better visual experience for comparing changes side-by-side. jj supports integration with many popular diff tools.

Let’s configure jj to use meld, a popular cross-platform diff viewer. If you don’t have meld, you can install it (e.g., sudo apt install meld on Ubuntu, brew install meld on macOS). If you prefer another tool like vscode, kdiff3, diffmerge, etc., the setup is similar.

Open ~/.jjconfig.toml one last time:

nano ~/.jjconfig.toml

Add the [merge-tools] section and configure meld, and then set it as your default diff-tool in the [ui] section:

# ~/.jjconfig.toml
# ... existing sections ...

[merge-tools]
# Define 'meld' as a merge tool
meld.program = "meld"
meld.args = ["$left", "$right", "$base", "$output"]
meld.diff = true
meld.merge = true
meld.conflict-markup = "diff3" # Use diff3 style for conflicts

# Set meld as the default for diffing
[ui]
# ... editor and log-template ...
diff-tool = "meld"

How it functions:

  • [merge-tools] is a section where you define the properties of external diff and merge tools.
  • meld.program = "meld" specifies the executable command for the tool.
  • meld.args = ["$left", "$right", "$base", "$output"] defines the arguments jj passes to meld. These placeholders are crucial for meld to understand which files to compare:
    • $left: Represents the “left” side of the comparison (e.g., the original file).
    • $right: Represents the “right” side of the comparison (e.g., the changed file).
    • $base: The common ancestor of the two files (used for 3-way merges).
    • $output: The file where the merged result should be saved (only used during merge operations).
  • meld.diff = true and meld.merge = true tell jj that meld is capable of handling both diffing and merging scenarios.
  • meld.conflict-markup = "diff3" specifies that jj should prepare files for meld using the diff3 style conflict markers, which meld understands well for 3-way merges.
  • [ui] diff-tool = "meld" then sets meld as the default tool jj should use when you run jj diff.

Save and close. Now, when you have changes and run jj diff, your configured graphical diff tool will open!

# Make some additional changes to a file
echo "yet another line" >> new_feature.txt

# Run diff
jj diff

Your meld (or chosen tool) should pop up, showing the differences!

Jujutsu’s Approach to Automation: Beyond Traditional Hooks

In traditional VCS like Git, “hooks” are scripts that run automatically at specific points in the workflow (e.g., pre-commit, post-merge). These are powerful but can also be complex to manage and rely on an immutable history model.

jj takes a different approach. It does not have traditional Git-style hooks. This is a deliberate design choice rooted in jj’s core philosophy of mutable history and a robust operation log.

Why jj avoids traditional hooks:

  • Mutable History: jj’s history is designed to be easily rewritten. Hooks tied to specific commit hashes or merge operations might become invalid or behave unexpectedly when history is regularly changed.
  • Operation Log: The jj operation log provides a powerful undo/redo mechanism for any action. This often replaces the need for pre-emptive hooks to “prevent” bad commits, as you can always jj undo or jj restore.
  • Atomic Changes: jj views the working copy as an ordinary commit, and operations are designed to be atomic and reversible. This reduces the need for external scripts to validate intermediate states.

How to achieve similar goals in jj:

  1. CI/CD Pipelines: For critical checks like linting, testing, and formatting, integrate them into your Continuous Integration/Continuous Deployment (CI/CD) pipeline. This is generally a more robust and scalable solution, as these checks run on a clean environment and provide consistent feedback for all contributors, independent of local jj configuration.

  2. External Scripting: Use build tools like make, just, npm scripts, or simple shell scripts to run checks before you execute a jj commit or jj new. You can even create a jj alias for this:

    # ~/.jjconfig.toml
    # ...
    [aliases]
    # An alias to run linting/testing before committing
    # (Replace 'npm run lint && npm test' with your project's actual commands)
    commit-checked = "sh -c 'npm run lint && npm test && jj commit'"
    

    You would then manually run jj commit-checked instead of just jj commit. This gives you explicit control over when checks run.

  3. Editor Integrations: Many modern editors have built-in linting and formatting on save, providing immediate feedback without needing a VCS hook. This is often the most immediate and least intrusive way to ensure code quality.

This approach aligns with jj’s principle of giving you powerful tools for history manipulation without locking you into a rigid, hook-based workflow.

Customizing Output with Templates and Revset Aliases

We briefly touched on log-template for jj log and jj status. jj offers a rich template language that allows you to fully customize how commit information is displayed. This is incredibly powerful for debugging, reviewing, and getting specific insights from your history.

What is jj’s Template Language?

jj’s template language is a powerful mini-language that lets you define exactly how jj formats output for commands like jj log, jj status, and jj diff. It uses curly braces {} for fields (like commit_id or description) and supports basic logic, filters, and formatting.

Why does it exist? The template language exists to give you granular control over jj’s output. Default outputs are good, but often you need specific pieces of information, formatted in a particular way, to quickly understand your repository’s state or a commit’s details.

What problem does it solve? It solves the problem of information overload or underload. You can tailor jj’s output to show precisely what you need, highlighting important details, filtering out noise, and presenting information in a consistent, readable format, whether for quick checks or detailed reviews.

Understanding and Using Templates

You can test templates directly on the command line using jj log -T followed by your template string.

Let’s try a more detailed template than our simple ui.log-template to see the power:

jj log -r @ -T '
📌 Commit: {commit_id.short} ({branches})
👤 Author: {author.name} <{author.email}>
🗓️ Date: {committer.timestamp.relative}
💬 Description:
{description}
📂 Files Changed: {files}
'

How it functions:

  • jj log -r @: This command tells jj to log the current working-copy commit (@).
  • -T '...': This flag specifies the template string to use for the output.
  • {commit_id.short}: Displays a shortened version of the commit ID, making it more readable.
  • {branches}: Shows any associated branch names that point to this commit.
  • {author.name} and {author.email}: Access specific fields within the author structure to display the author’s name and email.
  • {committer.timestamp.relative}: Shows the commit time relative to the current moment (e.g., “2 hours ago”, “yesterday”).
  • {description}: Inserts the full commit message.
  • {files}: Lists the paths of all files that were changed in this commit.

This gives you a much richer and more structured view of a single commit’s details.

Step-by-Step Implementation: Creating revset Aliases with Custom Templates

You can combine revsets with custom templates in your ~/.jjconfig.toml to create highly specialized views that are always just a short alias away.

Let’s create an alias jj mylog that shows a compact, yet informative, view of your recent commits, similar to a Git “oneline” log but with jj’s flavor.

Open ~/.jjconfig.toml:

nano ~/.jjconfig.toml

Add a new alias under your [aliases] section:

# ~/.jjconfig.toml
# ... existing aliases ...

[aliases]
# ...
# 'mylog' shows a concise log with short ID, branch, author, relative date, and first line of description.
mylog = "log -T 'commit_id.short} {branches} {author.name} {committer.timestamp.relative}\n{description.first_line() | indent(\"  \")}'"

How it functions:

  • mylog = "log -T '...'" defines the alias, telling jj to run log with a specific template.
  • commit_id.short}: Displays the abbreviated commit hash.
  • {branches}: Shows associated branch names.
  • {author.name}: Displays the author’s name.
  • {committer.timestamp.relative}: Shows the time of the commit relative to now.
  • {description.first_line()}: This is a template filter that extracts only the first line of the commit message.
  • | indent(" "): This is another template filter. It takes the output of the preceding field (description.first_line()) and indents it by two spaces, making the description visually distinct from the header line.

Save and close. Now, run jj mylog to see your custom log output!

jj mylog

This is just the tip of the iceberg for jj’s template language. Refer to the official Jujutsu documentation on templates for a full list of fields, filters, and advanced usage.

Environment Variables for Fine-Grained Control

While jjconfig.toml handles most configuration, some aspects can be controlled via environment variables. These are typically used for temporary overrides or for settings that interact with the system environment.

A few notable environment variables (though less frequently used for daily customization than jjconfig.toml):

  • JJ_EDITOR: Temporarily overrides the editor configured in jjconfig.toml for a single command.
  • JJ_PAGER: Specifies the pager program (like less) to use for jj’s output, overriding system defaults or jjconfig.toml settings.
  • JJ_REASON: Sets a default reason string for commands like jj undo, which usually prompt for a reason for the operation.

Example: Temporarily use vim as your editor for a specific commit:

JJ_EDITOR=vim jj commit

This command would open vim for the commit message, even if your jjconfig.toml specifies code --wait. This is useful for quick, one-off overrides without changing your permanent configuration.

Productivity Tips & Best Practices Recap

Beyond explicit customization, adopting certain jj workflows inherently boosts productivity and makes you a more effective developer:

  • 📌 Embrace the Operation Log: Make jj op log and jj undo your best friends. Don’t fear making mistakes; jj makes it trivial to revert almost any operation. This encourages experimentation and reduces mental overhead, knowing you can always go back.
  • 🧠 Leverage Stacked Changes: For feature development, create small, focused commits stacked on top of each other. This makes code reviews easier, allows for incremental testing, and gives you flexibility to jj rebase and jj squash them into a clean, linear history before sharing.
  • Think Branchless: jj’s model naturally encourages a more linear, branchless workflow. Instead of creating many short-lived Git branches, use jj new to create new working-copy commits directly and manage their position with jj rebase. This simplifies history and avoids “branch spaghetti.”
  • 🔥 Master revsets: The more comfortable you are with revsets, the faster and more precisely you can navigate, select, and manipulate commits. Practice simple revsets like @ (working copy), @- (parent), children(@) (children), root() (initial commit), and main() (the main branch head).
  • Regularly jj pull and jj rebase -r main: Keep your local history up-to-date with the remote main branch. This minimizes merge conflicts, keeps your work based on the latest codebase, and ensures your changes integrate smoothly.

Common Pitfalls & Troubleshooting

Even with powerful customization options, you might encounter a few bumps along the road. Here’s how to navigate common issues:

  1. Over-customizing or Conflicting Configurations:

    • Pitfall: Having too many aliases or overly complex templates can make jj harder to use, especially if you forget what your aliases do or if they conflict with future jj commands. Also, conflicting settings between your global ~/.jjconfig.toml and a repository-specific .jj/repo/config.toml can lead to unexpected behavior.
    • Troubleshooting:
      • Start with a few essential aliases and templates, then add more incrementally as you identify repetitive tasks.
      • If jj isn’t behaving as expected, check both your global ~/.jjconfig.toml and the repository-specific .jj/repo/config.toml (if you’re in a repo). Remember the hierarchy: repo config overrides user config.
      • You can temporarily disable your global config by moving it (mv ~/.jjconfig.toml ~/.jjconfig.toml.bak) or commenting out sections to see if the issue persists.
      • Use jj debug config to see the effective configuration jj is currently using, which can reveal overrides or unexpected values.
  2. Forgetting --wait for Editor Configuration:

    • Pitfall: If your editor configuration doesn’t include the --wait flag (or its equivalent for your chosen editor), jj might continue immediately after launching the editor, potentially leading to empty commit messages, premature command execution, or other issues.
    • Troubleshooting: Always ensure your editor command includes the necessary flag for jj to wait until the editor closes the file. Common ones are --wait (VS Code, Sublime Text, Atom), --listen (Neovim), or no flag for terminal-based editors like nano or vim.
  3. Complex Template Syntax Errors:

    • Pitfall: The jj template language can be powerful but also prone to syntax errors, especially with nested calls, complex filters, or incorrect field names. A small typo can break the entire template.
    • Troubleshooting:
      • Test complex templates incrementally on the command line using jj log -T '...'. Build them piece by piece.
      • Refer to the official template documentation for correct syntax, available fields, and filters. This is your authoritative guide.
      • Break down complex templates into smaller, simpler parts. If a filter isn’t working, test the field it operates on by itself first.

Summary

You’ve now learned how to personalize your jj experience, transforming it into an even more powerful and efficient tool for your daily development.

Here are the key takeaways from this chapter:

  • Configuration Hierarchy: jj uses ~/.jjconfig.toml for global settings and .jj/repo/config.toml for repository-specific overrides, with repo settings taking precedence.
  • Aliases for Productivity: Define custom shortcuts in the [aliases] section of your config file to simplify complex or frequently used commands, significantly boosting your typing speed and reducing errors.
  • Tool Integration: Configure jj to use your preferred text editor for commit messages ([ui] editor = ...) and external graphical diff tools ([merge-tools] and [ui] diff-tool = ...) for a seamless visual experience.
  • No Traditional Hooks: jj intentionally omits Git-style hooks due to its mutable history and robust operation log. Achieve similar automation goals through CI/CD pipelines, external scripts, or editor integrations.
  • Custom Output: Leverage jj’s powerful template language with jj log -T and revset aliases to tailor output formats for specific insights, debugging, and review processes.
  • Environment Variables: Use environment variables (like JJ_EDITOR) for temporary overrides of jj settings without altering your permanent configuration.
  • Best Practices: Reinforce good jj habits like embracing the operation log, leveraging stacked changes, thinking branchlessly, and mastering revsets for maximum productivity and a smoother workflow.

With these customization techniques, you’re well on your way to becoming a jj power user, optimizing your workflow for speed, clarity, and control.

What’s next? In our final chapter, we’ll bring everything together, discussing how jj scales to large projects, advanced debugging strategies, and a comprehensive guide to migrating from Git, solidifying your journey to jj mastery!

References

This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.