Requirements

Node 18 or newer, and git on your PATH. That's the whole list — diffotator has no npm dependencies.

macOS, Linux and Windows. The browser is whatever open / xdg-open / start launches.

Install

Clone the repository, then link the binary and the slash command:

git clone https://github.com/adisagar2003/diffotator.git
cd diffotator

# link the CLI and install the /diffotator command
ln -sf "$PWD/bin/diffotator.js" ~/.local/bin/diffotator
cp claude/diffotator.md ~/.claude/commands/diffotator.md

# optional: open a review automatically after every turn
diffotator hook --install

There is nothing to build and nothing to install into the repository you review — the clone is the install.

Check it resolves:

$ diffotator --help
Make sure ~/.local/bin is on your PATH. If diffotator isn't found, add export PATH="$HOME/.local/bin:$PATH" to your shell profile.

Run the test suite if you want to confirm the git layer behaves on your machine:

$ node test.js
ok — all checks passed

Claude Code integration

The integration is one slash command. claude/diffotator.md installs as /diffotator and tells Claude to run the CLI in the background, wait, and act on whatever comes back:

---
description: Open the current diff in diffotator for review
allowed-tools: Bash(diffotator:*)
---

Run `diffotator $ARGUMENTS` in the background (it blocks until the review
session is submitted, which can exceed the Bash timeout)…

Then in any session:

/diffotator
/diffotator --base origin/main

The CLI prints its URL on stderr and the review result on stdout, so the feedback arrives clean. It blocks until you submit, then exits.

Other agents work too. Nothing here is Claude-specific — any harness that can run a command and read its stdout can use diffotator. The slash command is just the thinnest possible wrapper around that contract.

Automatic review (Stop hook)

A review tool you have to remember to run reviews nothing. Install the hook and every turn that leaves real changes gets looked at, without you typing anything:

$ diffotator hook --install
Installed the diffotator Stop hook in ~/.claude/settings.json
Reviews open when a turn leaves 3+ changed files.

It appends to hooks.Stop in ~/.claude/settings.json, leaving any hooks you already have in place, and writes a settings.json.diffotator-backup first. diffotator hook --uninstall removes just its own entry.

What the agent gets

You pressHook returnsEffect
Send feedback{"decision":"block","reason":…} Turn is blocked; your review becomes the reason and the agent keeps working
Approve{}Turn ends normally
Close{}Turn ends; nothing sent

When it stays quiet

This is the part that decides whether the hook is useful or maddening. A review only opens if it survives all four gates:

  • Clean tree — nothing uncommitted, nothing to review.
  • Below threshold — fewer than DIFFOTATOR_HOOK_MIN_FILES changed files (default 3). A one-line turn is noise.
  • Unchanged since your last review — the working tree is fingerprinted by content, so approving a review and letting the agent add one more sentence will not pop the browser back open. Editing a file in place does count as new work, even though the file count is identical.
  • Loop guard — if the harness reports that a Stop hook already forced a continuation, this one allows immediately.
Tuning it. DIFFOTATOR_HOOK_MIN_FILES=1 to review everything, DIFFOTATOR_HOOK=off to disable without uninstalling, and DIFFOTATOR_DEBUG=1 to print the gate decision to stderr.

Scopes

A scope is what you're reviewing. The sidebar offers three, and everything downstream — the file list, the diffs, the file tree — is expressed in terms of the one you pick.

ScopeShows
Local Changes Working tree vs HEAD, including untracked files. The default, and what you want straight after an agent run.
vs <base> The whole branch against its base, auto-detected from your upstream then origin/main, origin/master, origin/develop. Override with --base.
All Commits The commit graph. Selecting a commit reviews that commit alone, against its first parent.

Below those, the sidebar lists worktrees, branches, tags, stashes and remotes. Selecting one loads its history into the commit list.

The All Commits scope: commit graph with branch lanes and refs, and the diff of the selected merge commit below it.

A merge selected in All Commits — shown against its first parent, so you see what the merge brought in.

Base detection skips your own remote branch. origin/<your-branch> is the same line of work, so diffing against it only shows what you haven't pushed — not a review.

The review loop

Built for the case where an agent just touched fifty files:

  1. Read the file. n and p jump between changes inside it.
  2. Something wrong? c on the focused line, or click any line number.
  3. v marks the file viewed and jumps to the next unviewed one.
  4. Repeat until the progress bar reads 56/56.
  5. Send feedback, or Approve if there's nothing to say.

Viewed files dim with a ✓ and the header tracks 12/56 viewed +2,341 −187. Viewed state is kept per scope — having read a file in the working tree isn't the same as having read it in a commit.

Progress and unsent comments persist to ~/.local/share/diffotator and survive across separate runs, not just reloads.

Why not localStorage? It is scoped to the origin including the port, and every run binds a fresh random port — so drafts written by one session were invisible to the next. Disk also works when the browser is on a different machine.

Comments

Click any line number in either gutter, or press c. Labels follow Conventional Comments:

suggestion nit question issue praise thought note todo chore

  • Blocking flag. Blocking comments are counted and called out at the top of the feedback, and the agent is told they must be resolved.
  • Suggested code. An optional replacement block, delivered as a fenced code block alongside the comment.
  • Old or new side. Commenting on a deleted line is recorded as (old side) so the agent knows which version you mean.
The comment popover open on a line: label chips, the comment body, a blocking checkbox and a suggested replacement.

Label, body, blocking flag, suggested replacement — and the comment already saved on this file rendered inline behind it.

Saved comments render inline under the line they discuss, with edit and delete. The Comments panel lists them all and jumps to any one — including comments on files outside the current change set.

Closing discards. Close sends nothing to the agent. If you have unsent comments it asks first, but there's no draft handoff — use Send feedback to deliver them.

Browsing beyond the diff

Three tabs sit under the commit list:

  • Changes — the changed files for the current scope, plus the diff.
  • Commit — author, date, refs, sha, parents and the full message.
  • File Tree — every file in the repository at that revision, including untracked ones. Open any file and comment on it, even if the change never touched it.

The file pane is a flat List by default, because reviewing is working through a list. Switch to Tree for the repo browser; single-child directory chains fold into one row either way.

Full file keeps the diff marks and stops folding, so you can read a change in the context of everything around it.

Keyboard

KeyAction
⌃h ⌃j ⌃k ⌃lMove between panes (⌃ + arrows too)
j kNext / previous file
n pNext / previous change in the file
Move the line cursor
cComment on the focused line
vMark viewed, jump to next unviewed
sSplit / unified
fFull file / diff only
tComments panel
/Filter the file list
⌘FFind in the current file
⌘↵Save comment, or send the review
EscClose the popover, search or dialog
?Shortcut sheet
Why ⌘F is ours. Only the visible rows exist in the DOM, so the browser's own Find genuinely can't see the rest of the file. The built-in search scans the whole file and steps through matches.

CLI

diffotator [review] [options]      # open a review now
diffotator hook                    # Stop-hook mode (hook JSON on stdin)
diffotator hook --install          # add the Stop hook
diffotator hook --uninstall        # remove it
FlagMeaning
-C, --cwd <dir>Repository to review. Default: current directory.
-b, --base <ref>Base ref for the branch scope. Default: auto-detected.
-p, --port <n>Port to listen on. Default: a random free port.
--no-openDon't launch a browser; just print the URL.
--title <s>Header title for the session.
-h, --helpUsage.

Reviewing a pull or merge request

There's no built-in fetching. Check it out locally and point diffotator at the base — this also gets you the full working tree to browse, which a diff-only view can't:

gh pr checkout 123   # or: glab mr checkout 123
diffotator --base origin/main

The server binds to 127.0.0.1 only.

Output contract

diffotator blocks until you submit. The URL goes to stderr; the result goes to stdout.

You pressstdout
Send feedbackA # Code review feedback markdown document
ApproveThe user approved.
Close / Ctrl-CReview session closed without feedback.

Exit code is 0 in every case — the outcome is in the text, not the status. The document groups comments by file, orders them by line, and puts a blocking count above the first one.

Shape of the document

# Code review feedback

Reviewed `repo` — origin/main...HEAD. 2 comments.

## Overall

<your summary, if you wrote one>

> **1 blocking comment** — these must be resolved.

## `path/to/file.ts`

### path/to/file.ts:23 — issue (blocking)

*L23*

```ts
<the line you commented on>
```

<your comment>

Suggested replacement:

```ts
<your suggested code, if any>
```

---

Address every comment above…

How it stays fast

The usual reason a browser diff viewer feels slow is that it ships the entire review to the page as one document. diffotator sends nothing up front.

  • Per-file, on demand. Git data is fetched when you open a file, and the next file is warmed in the background so j feels instant.
  • Windowed rows. The commit list and the diff render only the visible slice. A 61,000-line minified diff opens with no stall.
  • Derived heights. Code rows are uniform; inline comment cards get a height computed from an estimated wrap rather than measured, so the prefix-sum index that drives windowing stays exact and scrolling never jumps.
  • Highlighting on visible rows only, from a small regex tokenizer — there's no grammar bundle to download.
  • Full context in one call. git diff -U1000000 per file, so expanding folded regions needs no round trip.

Limits

Deliberately not built — use your git client:

Staging, committing, push and pull, rebase, blame, merge conflict resolution, image diffs, and fetching pull requests from GitHub or GitLab.

Known edges

  • Merge commits are shown against their first parent — what the merge brought in.
  • Diffs over 200,000 changed lines and files over 4 MB are reported rather than rendered.
  • Binary files are detected and skipped; there's no image diff.
  • One session at a time per invocation. Submitting ends the process — that's the contract that lets the agent read the result.

Troubleshooting

"is not a git repository"

diffotator resolves the repo root from the working directory. Use -C to point it elsewhere.

The "vs base" scope is missing

No suitable base was found. Every candidate was either your own remote branch or had no commits between it and HEAD. Pass one explicitly with --base origin/main.

The browser didn't open

The URL is printed on stderr — open it by hand. --no-open makes that the default behaviour.

The command seems to hang

That's the contract: it blocks until you submit in the browser, which is how the agent gets your feedback. Run it in the background, or Ctrl-C to close the session without sending anything.

My comments disappeared

Drafts live in ~/.local/share/diffotator/drafts, keyed by repository, and are cleared once a review is submitted. Override the location with DIFFOTATOR_DATA_DIR.

The hook keeps opening a browser

Raise DIFFOTATOR_HOOK_MIN_FILES, or set DIFFOTATOR_HOOK=off. Run with DIFFOTATOR_DEBUG=1 to see why the gate fired.

The hook never opens

DIFFOTATOR_DEBUG=1 diffotator hook <<< \'{"cwd":"$PWD"}\' prints the gate decision — usually clean-tree, below-threshold or unchanged-since-review.