Review what your coding agent changed — without leaving the terminal.
install · review loop · agents · keys · config · contributing
You read the changes file by file, comment on the lines that need it, and send those comments to the agent's pane in one keystroke. differ tracks what you have read and carries the result; it does not review anything for you.
- comment on the line, send in one key — comments sit inline under the
line they are about;
Ssends them as references the agent can open. review loop → - works with herdr and tmux — finds the agents running beside you, sends the review to the one you pick, and under herdr follows it until it answers. Nothing to configure. agents →
- remembers where you got to — every file is
read,commented,sentorchanged, and the review survives a crash or a quit. - the diff does not move under your comments — it follows the agent live until you comment, then holds your place and tells you it moved.
- opens your editor at the line — reusing an nvim already open in the same tmux session or herdr workspace. editor →
- an ordinary diff viewer too — syntax highlighting, split view, staging, committing, branches, commit log. keys →
- one Go binary — no model reads your diff; nothing leaves your machine except what you send.
brew install jansmrcka/tap/differ
go install github.com/jansmrcka/differ@latestOr from source: make build (→ bin/differ) or make install (→ $GOPATH/bin).
differ # all changes (staged + unstaged + untracked)
differ -s # staged only
differ -r main # compare against a branch, tag or commit
differ -c # open straight into the commit message
differ review # the same changes, straight into the first diff
differ log # browse recent commits
differ commit # review what is staged, then commitreview takes -s and -r too. --no-color, or NO_COLOR set to anything,
turns colour off — nothing in differ is distinguished by colour alone.
Exit codes: 0 fine, 1 a runtime problem (not a repository, no such ref), 2 a bad command line.
differ review # the agent's changes, opened on the first diff
j / k # move down the diff
c # comment on this line — type, then ctrl+s
J # next file; the one you left is marked read
S # send every pending comment to the agent
What that gives you over git diff:
- It remembers where you got to. Every file is
unreviewed,read,commented,sentorchanged, and the bar keeps the count:3/7 reviewed 2 comments 1 pending 1 changed. - Comments attach to lines. They show inline under the line they are about
and stay
pendinguntil you send them. - The diff does not move under your comments. A diff with nothing written
against it follows the agent live. Once you have commented on a file, an
agent rewrite keeps your place and the bar says
diff moved (2 more added) — R to reload. Comments follow their line either way; one whose line is gone goesstalerather than pointing at whatever took its place. - They survive
q. The review is written to.git/differ/review.jsonon every change, so a crash, a closed tmux window or an accidental quit costs nothing. A comment comes back only if the code it was written about is byte-for-byte unchanged — otherwise the file goes back to unreviewed, because it needs reading again.
Nothing here touches git. Staging and committing stay explicit.
A lists the coding agents running in tmux, herdr or zellij and
sends the review to the one you pick. Which of them is detected from the
environment, nothing to configure; inside none, the review goes to the
clipboard or stdout.
agent
filter… 4/4
▌ differ:2 claude /Users/you/git/private/differ
ELI-panda:2 claude /Users/you/git/work/ELI-panda
personal-web:2 %5 opencode /Users/you/git/private/personal-web
personal-web:2 %6 claude /Users/you/git/private/personal-web
type filters · ↑/↓ · enter chooses · esc closes
Typing narrows the list — by session or workspace, agent, state or title — and
esc clears the filter before it closes the picker. The choice applies at
once and is written to the config, so it survives a restart.
Under tmux the agent in differ's own tmux session sorts first, then one
working in this repository; two in the same window are told apart by pane id.
Discovery walks each pane's process tree rather than reading its current
command, so an agent started through npx, a shell function or a wrapper
script is still found. It knows claude, codex, gemini, copilot,
opencode and aider, including forms like npx @anthropic-ai/claude-code
and uvx --from aider-chat aider. It never starts an agent, and it will not
offer a one-shot such as claude -p. The review is pasted, not submitted:
tmux cannot tell an agent from a shell, so differ never presses Enter.
Under herdr herdr is asked (herdr agent list), so each row also shows
the agent's state and what it says it is working on, and an agent that is
blocked, idle or done sorts above one that is working. The review is
submitted with herdr agent prompt, which refuses before sending anything
if the agent is blocked on a question — the comments then stay pending and the
bar says why. differ remembers the agent by its own session id, so moving its
pane does not lose it.
After a send under herdr, differ follows the agent: the comments and H say
agent working, then agent answered (or agent waiting for you), and the
bar points out a reviewed file that changed in the meantime.
What the agent receives, per comment:
@src/session.ts :L8
this drops the error instead of returning it — the caller cannot tell
The reference carries the file and the line, so nothing repeats them and the
agent reads the code itself. It is the form sidekick.nvim emits, and the
space before the colon matters: without it the resolver reads @path:9 as
one filename and attaches nothing. Claude Code's Neovim integration does not
use this form at all — it sends JSON-RPC over a websocket, which is a
different channel.
How much of a reference a comment gets depends on what still resolves:
| the line resolves | @src/cache.ts :L12 |
| only the file does | @src/cache.ts |
| nothing does | no reference; the prose still says where it was |
The middle case is a line number that would be wrong: the old side of a diff,
or the index under -s when the working tree has moved on. Those comments
carry the hunk instead, since it is then the only thing that says which code
is meant:
@src/api/client.ts
Line: 10 (old)
Changed code:
- return fetch(url)
+ return await fetch(url)
Comment:
this needs awaiting too
s sends one comment, S all of them, H shows what was sent and whether it
arrived. A failed send never discards anything — the comments stay pending.
differ pastes into the agent's prompt and does not press Enter, so a
misconfigured target cannot execute anything.
? lists the current view's keys. The bar along the bottom shows the common
ones and hides what would do nothing; a picker or input shows its own. The
tables below are checked against the code — a test fails if a key here has no
handler, or a handler is missing here.
Except while typing.
| Key | Action |
|---|---|
? |
every key for this view |
H |
what has been sent, and whether it got there |
! |
the last failure, in full |
t |
theme picker |
A |
choose the agent reviews go to |
ctrl+c |
quit immediately |
● M login.ts +12 -4
M a/index.ts +8 -2
A format.ts +31 -0
? NOTES.md +3 -0
● is staged. Once a file has been read, the right-hand column shows review
state instead — read, 2 comments, sent, changed.
| Key | Action |
|---|---|
j/k |
navigate files |
enter / l |
view diff |
tab |
stage/unstage file |
a |
stage all |
C |
commit (AI-generated message via claude) |
b |
open branch picker |
v |
toggle split (side-by-side) diff |
e |
open in editor — differ keeps running |
P |
push (auto --set-upstream if needed) |
F |
pull (fast-forward only) |
g/G |
first/last file |
q |
quit |
Reading and reviewing are one view: comment on the line you are reading.
Nothing here changes git state except tab.
| Key | Action |
|---|---|
j/k |
move line cursor |
} / { |
next/prev hunk |
d/u |
half page down/up |
g/G |
first/last line |
J/K |
next/prev file |
c |
comment on line (edit existing) |
C |
comment on whole hunk |
x |
delete comment under cursor |
s |
send comment under cursor |
S |
send all pending comments |
R |
reload after the file changed |
tab |
stage/unstage |
b |
open branch picker |
v |
toggle split diff |
e |
open in editor at the cursor's line |
esc / h |
back to file list |
q |
quit |
Marks in the gutter: ▌ the line cursor, ● a comment on this line, ! a
comment that no longer matches the diff. At the end of the code: › the line
was cut to fit, · trailing whitespace.
Split view shades the part of a line that actually differs, so a one-character change does not read as a rewrite. It falls back to unified when the panel is too narrow for two columns.
| Key | Action |
|---|---|
enter |
confirm commit |
esc |
cancel |
╭───────────────────────────────────────────────────────╮
│ branch │
│ │
│ > feat/ 12/34 │
│ │
│ ▌* feat/roadmap-59 │
│ feat/payload-90 │
│ feat/agent-picker-84 │
│ │
│ type filters · ↑/↓ · enter switches · ^n new · esc │
╰───────────────────────────────────────────────────────╯
| Key | Action |
|---|---|
| type | filter branches |
up/down |
navigate |
ctrl+k/ctrl+j |
navigate |
enter |
switch branch (asks again if uncommitted changes would move) |
ctrl+n |
create new branch |
esc |
clear filter / close |
differ log browses recent commits; enter shows one through the same diff
renderer.
| Key | Action |
|---|---|
j/k |
move through commits |
g/G |
newest/oldest |
enter |
view the commit's diff |
esc |
back to the list, from a diff |
q |
quit |
differ --theme gruvboxmocha (default, also dark), latte (also light), gruvbox,
tokyonight, github. Each is paired with the matching Chroma style so the
chrome and the syntax highlighting agree, and all five are held to a contrast
floor by a test.
t opens a picker that repaints the screen as you move, so you choose by
looking rather than by name. enter keeps it and writes it to the config.
Config file: ~/.config/differ/config.json
{
"theme": "mocha",
"tab_width": 4,
"commit_msg_cmd": "claude -p",
"commit_msg_prompt": "Write a concise git commit message for this diff:",
"editor_cmd": "",
"editor_strategy": "auto",
"editor_panes": [],
"editor_target": "session",
"editor_line_args": "",
"editor_timeout_ms": 0,
"editor_probe_timeout_ms": 0,
"split_diff": false,
"feedback_target": "clipboard",
"tmux_target": "",
"herdr_target": "",
"herdr_pane": "",
"zellij_target": ""
}feedback_target is where a review goes: clipboard (default), stdout,
tmux, herdr or zellij. A sets it for you, along with tmux_target, zellij_target or the herdr
agent's session id (herdr_target) and the pane it was in (herdr_pane). The clipboard target shells out to pbcopy or
wl-copy/xclip/xsel, so it does the right thing over SSH.
commit_msg_cmd generates a commit message from the staged diff when you
press C; it is pre-filled for you to edit. Any command that reads a diff on
stdin works, and differ carries on without one.
e opens the file under the cursor — at the cursor's line, from the diff or a
review. differ keeps running.
Which editor: editor_cmd, else $EDITOR, else vi. editor_cmd supports
{file}, {repo} and {line}, substituted inside a single argument so
spaces need no quoting. Without {file} differ adds the line itself for
editors it knows (+<line> for vi-likes, --goto for code);
editor_line_args is the escape hatch for one it does not:
{ "editor_line_args": "{file}:{line}" } // helix, sublime
{ "editor_line_args": "+{line} {file}" } // emacs, microWhere it opens — editor_strategy:
| value | what happens |
|---|---|
auto (default) |
reuse an nvim already open in this tmux session or herdr workspace → else a new tmux window or a pane beside differ in herdr → else take over differ's terminal |
reuse |
only reuse; say so when there is nothing to reuse |
window |
always a new tmux window (a new pane in herdr) |
inline |
take over differ's terminal, resume when the editor exits |
detach |
run it in the background and carry on |
Started from nvim's own :terminal ($NVIM is set, e.g. differ in a float
over your editor), auto and reuse open the file in that nvim instead: a
float closes, a split hands focus back, and differ keeps running in the hidden
terminal buffer.
Outside tmux and herdr, reuse and window are refused with a message
rather than silently downgraded. Reuse needs nvim's RPC socket and sends
:drop, so nothing unsaved is ever at risk, then focuses that pane. In herdr
it never leaves differ's workspace unless editor_target says so; the new
pane is quoted for the shell it runs, fish included. editor_panes and editor_target (any, or a session /
workspace name) adjust which panes it will consider and how far it reaches.
GUI editors reuse their own window already, so give them detach:
{ "editor_cmd": "code -r --goto {file}:{line}", "editor_strategy": "detach" }
{ "editor_cmd": "zed {file}:{line}", "editor_strategy": "detach" }An editor_cmd starting with tmux runs exactly as written and ignores
editor_strategy.
The layout differ is built for is an agent in one pane and differ in another. It works the same inside tmux or herdr, and detects which from the environment:
| tmux | herdr | |
|---|---|---|
| finding agents | walks each pane's process tree | asks herdr agent list, with each agent's state |
| sending | pasted, never submitted | submitted with herdr agent prompt |
| after a send | — | follows the agent: working → answered |
e |
reuses nvim in the session, else a new window | reuses nvim in the workspace, else a pane beside differ |
Zellij is supported for sending reviews: A lists the panes whose
foreground command is an agent (zellij action list-panes), and the review is
pasted with zellij action paste and never submitted, as in tmux. The choice
is stored as zellij_target. e does not know Zellij yet.
To open differ as a tmux popup:
bind g display-popup -E -w 90% -h 90% "cd #{pane_current_path} && differ"One line, in differ's own voice, with what to do:
push failed · ! details · the remote has commits you do not — pull with F first
! shows what git, tmux or the clipboard helper actually said. A failure
costs you nothing else — not your place in the diff, not the comments you have
written.
Contributions are very welcome — bug reports, ideas, docs, a new theme, support for another agent, or code. You do not need to ask first: open an issue or a pull request.
- New here? Pick a good first issue or one marked help wanted.
- Found a bug or want something? Open an issue — a screenshot or the exact keys you pressed helps most.
- Want to send code? CONTRIBUTING.md takes you from fork to pull request in a few commands.
If you are an AI agent working on this repository, read
CLAUDE.md first: it has the rules that are not obvious from
the code.



