Someone in the [Indy Hackers Slack](https://slack.indyhackers.org/) shared the following post:

<blockquote class="mastodon-embed mastodon-embed-fallback" data-embed-url="https://mastodon.social/@rands/115577562153654788/embed" style="background: #FCF8FF; border-radius: 8px; border: 1px solid #C9C4DA; margin: 0; max-width: 540px; min-width: 270px; overflow: hidden; padding: 0;">
<div style="padding: 20px;">
<p lang="en" dir="ltr">My first Gemini CLI experience: "I messed up. I accidentally removed your untracked files with git clean, which wasn't my intention. I'm truly sorry. Since they weren't in git, I can't restore them easily."</p>
<p>&mdash; rands (@rands@mastodon.social) <a href="https://mastodon.social/@rands/115577562153654788">View on Mastodon</a></p>
</div>
</blockquote>
<script data-allowed-prefixes="https://mastodon.social/" async src="https://mastodon.social/embed.js"></script>

I had also run into this problem while coding with AI agents. This seemed like an opportune time to write up some thoughts to hopefully save others some time and agony.

Sometimes this issue happens to me because I get too confident in the current work direction and have not committed in a while. And then one or two prompts later, I have a broken or confusing set of changes.

Or, as Rands's post mentions, sometimes the AI agent purposely or accidentally reverts some changes and can't recover them. Also, when Claude Code compacts context, it clears the terminal, so it often can't remember changes, and then we would be unlikely to recover.


### Enter `jj`

To try to combat this problem, I've been setting up and using [Jujutsu](https://github.com/jj-vcs/jj) (henceforth `jj`) instances for the repos that I have. `jj` snapshots the working copy whenever you run a `jj` command, so you can use its history to see and restore changes that might have otherwise gotten lost. It also works fairly seamlessly with `git`, which basically all of my projects use at this point. It also doesn't affect other users of your projects. So in my mind there are basically no downsides to setting it up.

<!-- more -->

It's easy to set up `jj` alongside an existing `git` repo with `jj git init --colocate`.

Then there are a few commands that I've been using. I just set it up on my blog repo, and have the following info:

```
$ jj
@  lrklqzxy panozzaj@gmail.com 2025-11-22 16:06:25 b76e8471
│  (no description set)
◆  qppwxvzp panozzaj@gmail.com 2025-11-06 10:57:53 master master@origin git_head() e9476b33
│  Add site perf audit document
...
```

```
$ git log --oneline --graph --decorate | head 1
* e9476b3 (HEAD -> master, origin/master, origin/HEAD) Add site perf audit document
```

So you can see from the `jj` command output that it knows about `git` commit `e9476b3` (`jj`'s identifier for that commit is `qppwxvzp`).

One thing I almost immediately picked up on is that `jj` commit identifiers use characters in the range of `[g-z]`, which is nice since `git` uses the hex characters (`[0-9,a-f]`) for its commit hashes. So this makes it easier to not mix up the two systems' unique identifiers. `jj` also highlights the unique starting characters in a different color, so it's easy to type out a couple of characters:

![jj screenshot showing color-coded commit identifiers](/images/jj_screenshot.png)

From above, `jj` has a working set of changes that it currently describes with `lrklqzxy`/`b76e8471`. Or, at least it did, until I just saved this draft. Now the `git` identifier is something else, since the hash of the underlying filesystem changed.

The uncommitted set of changes is aliased to `@`. It's similar to the `git` working directory, but each filesystem change is actually "committed" under the hood. To save the current set of changes in `jj` and change `@`s identifier, we'd use [`jj describe`](https://steveklabnik.github.io/jujutsu-tutorial/hello-world/describing-commits.html) to write a commit message.

Here's where the work-saving ability comes in. With `jj`, we can view the last couple of repo changes:

```
$ jj obslog --revision @ --patch --limit 2
@  lrklqzxy panozzaj@gmail.com 2025-11-22 16:16:46 2583d144
│  (no description set)
│  -- operation 213466f024c9 snapshot working copy
│  M _drafts/using-jujutsu-jj-with-ai-coding-agents.markdown
│  Modified regular file _drafts/using-jujutsu-jj-with-ai-coding-agents.markdown:
│      ...
│    37   37: * e9476b3 (HEAD -> master, origin/master, origin/HEAD) Add site perf audit document
│    38   38: ```
│    39   39:
│    40     : So you can see from the `jj` command output that it knows about git commit `e9476b3` (`jj`'s version of this is `qppwxvzp`), and has a working set of changes that it currently describes with `b76e8471`. Or, at least it did, until I just saved this draft. Now it's something else. So that working set is nicknamed `@`, and it's a pretty useful concept.
│         40: So you can see from the `jj` command output that it knows about git commit `e9476b3` (`jj`'s version of this is `qppwxvzp`).
│         41:
│         42: One thing I almost immediately picked up on is that the `jj` commit descriptors uses characters in the range of `[g-z]`, which is nice since `git` uses the hex characters (`[0-9][a-f]`) for its commit hashes. So this means that you'll never get the two systems' identifiers mixed up. It's not visible here, but `jj` also highlights the unique starting characters in a different color, so it's easy to type out a couple of characters.
│         43:
│         44: From above, `jj` has a working set of changes that it currently describes with `lrklqzxy`/`b76e8471`. Or, at least it did, until I just saved this draft. Now the git portion of it is something else, since the hash of the underlying filesystem changed. The uncommitted set of changes is nicknamed `@`, and it's a pretty useful concept. Similar to the `git` working directory, but each filesystem change is actually "committed" under the hood.
│         45:
│         46: To see this, we can see the last few changes:
│         47:
│    41   48:
│    42   49:
│    43   50:
│      ...
○  lrklqzxy hidden panozzaj@gmail.com 2025-11-22 16:10:37 1915095f
│  (no description set)
│  -- operation 217810a349cf snapshot working copy
│  M _drafts/using-jujutsu-jj-with-ai-coding-agents.markdown
│  Modified regular file _drafts/using-jujutsu-jj-with-ai-coding-agents.markdown:
│      ...
│    37   37: * e9476b3 (HEAD -> master, origin/master, origin/HEAD) Add site perf audit document
│    38   38: ```
│    39   39:
│    40   40: So you can see from the `jj` command output that it knows about git commit `e9476b3` (`jj`'s version of this is `qppwxvzp`), and has a working set of changes that it currently describes with `b76e8471`. Or, at least it did, until I just saved this draft. Now it's something else. So that working set is nicknamed `@`, and it's a pretty useful concept.
│    41   41:
│    42   42:
│    43   43:
│      ...
```

So this command (`jj obslog --revision @ --patch --limit 2`) basically says:

 - show me the operations         (`obslog`)
 - starting with revision `@`     (`--revision @`)
 - show diffs                     (`--patch`)
 - limit to the last two changes  (`--limit 2`)

Finally, when you make changes to the `git` repo, `jj` is kept in sync with it.

Long-time users of `git` might see this as being similar to the `git svn` bridge for Subversion. You get the advantages of working with `git`, but Subversion stays the source of truth for collaboration.


### Advantages of using

I started using `jj` before Claude Code had its [checkpointing](https://code.claude.com/docs/en/checkpointing) or rewind feature. I still use this regularly since:

1. If you get out of the context window, you can still see past changes (which I am not confident rewind handles correctly, and as previously mentioned, at one point my console history would clear when context was compacted)
2. It works across editors / agents, so you don't need to rely on them implementing checkpoints. Also, if I accidentally lose a file using `sed` or `mv` or something, I could likely get it back correctly.
3. You don't need to be an expert with `jj` to use it. If you get in trouble, your agent should know enough to use `jj` to get those changes back if asked, and otherwise seems to not be trained to use it, so it won't be committing there.

I don't yet fully grasp all of the underlying concepts to be able to use advanced `jj` correctly, but for this one case (recovering uncommitted changes that I actually wanted) it's saved me a couple of times. And it's just kind of a cool tool.

I suspect that there are things that I could do with using this to tracking incremental changes in `jj` and then batching those up for `git` commits. Almost like `git add --patch` and committing, or by saving semantic changes along the way. (Maybe this would be a useful place for the agent to track detailed change history?)


### Making snapshots automatic

**Update (2026-01-23):** One thing I got wrong in my original post: `jj` doesn't have a background daemon watching for file changes. It snapshots the working copy when you run a `jj` command (like `jj status`, `jj log`, or even just `jj`). So if an agent makes changes and then crashes before any `jj` command runs, those changes won't be in the history.

To make snapshotting more automatic, I use two approaches:

**1. Shell hook** - Add `jj status` to your shell's `preexec` so it snapshots before each command runs:

```zsh
preexec () {
  # Snapshot jj repo before command runs
  [[ -d .jj ]] && jj status >/dev/null 2>&1
}
```

Using `preexec` instead of `precmd` means it only runs when you actually execute a command, not on prompt redraws or empty enters. This captures the state right before a command you type might change things.

**2. Claude Code hooks** - Snapshot at high-risk moments (session start and before context compaction).

In `~/.claude/settings.json`:

```json
{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "*",
        "hooks": [{ "type": "command", "command": "[[ -d .jj ]] && jj status" }]
      }
    ],
    "PreCompact": [
      {
        "matcher": "*",
        "hooks": [{ "type": "command", "command": "[[ -d .jj ]] && jj status" }]
      }
    ]
  }
}
```

The shell hook captures changes after every command you or the agent runs. The Claude Code hooks specifically target the moments when context loss is most likely:
 - at session start: before the agent might delete something without having read the contents (`git reset`, for example)
 - before compaction (when the agent might forget what it had been reading).

Adding a `PreToolUse` hook that runs `jj status` before each tool call works, but it adds a bit of latency. It seems like running `time jj status` only takes about 0.01s, but there might be some overhead in invoking the hook itself. Maybe not an issue if you have other `PreToolUse` hooks, since I believe they run in parallel.


### References

I think I originally got the idea of using `jj` for this purpose on some Hacker News comment, possibly [this one](https://news.ycombinator.com/item?id=44645239):

> Eh, I can see how, if you use GitButler, the porcelain is fairly irrelevant to you, but a few days ago I decided to try Jujutsu, asked Claude how I could do a few things that came up (commit, move branches, push/pull to Github). It took me ten minutes to become proficiend in Jujutsu, and now it's my VCS of choice.
>
> I still use Lazygit for the improved diffing, but, as long as you don't mind being in detached HEAD all the time, there's really no issue with doing that. JJ interoperates fine with git, but why would I use the arcane git commands when JJ will do the same thing much more straightforwardly?
>
> Also, the ability to jump from branch to branch with all my uncommitted files traveling with me is a godsend. Now I can breeze between feature development, bug fixing, copy changing, etc just by editing the commit I want. If I want multiple AI agents working on that stuff, I just make a worktree and get on with it.
>
> Not to mention that I am really liking the fact that I can describe changes (basically add commit messages) before I'm done with them, so I can see them in the tree.
>
> JJ is just all around great.

Although [a newer comment](https://news.ycombinator.com/item?id=45055550) also captures this idea, and is perhaps better documented (though see my update above—the "automatically captured" claim is slightly misleading):

> With jj, every file change is automatically captured (no manual commits needed), and you can create lightweight "sandbox" revisions for each Claude Code task. When things go wrong, `jj undo` instantly reverts to any previous state. The operation log tracks everything, making it virtually impossible to lose work.
>
> The workflow becomes: let Claude Code generate messy experimental code → use `jj squash`/`jj split` to shape clean commits afterward. You get automatic checkpointing plus powerful history manipulation in one tool.
>
> I've been using jj with Claude Code for months and it's transformed how I work with coding agents - no fear of breaking things because everything is instantly reversible. The MCP integration seems like added complexity when jj's native capabilities already handle the core problem.
>
> For anyone interested in the jj + agent workflow, read my post: <a href="https://slavakurilyak.com/posts/use-jujutsu-not-git" rel="noopener">https://slavakurilyak.com/posts/use-jujutsu-not-git</a>

Also, I [wrote about making commits while writing](/blog/2025/11/22/avoid-losing-work-with-jujutsu-jj-for-ai-coding-agents) a very long time ago, and it would now seem fairly obviated with `jj`.
