CLI reference
One diffo binary serves both sides of the review: you open reviews with it, and the agent that wrote the code drives the feedback loop through it. The Agent Skill teaches your agent everything on this page.
diffo [options] # open (or resume) the review for this repo
diffo <target> [options] # review against a branch, or review a pull request
diffo <command> [...] # manage the server, or talk to the review
diffo help [command] # help for one commandNothing needs installing: npx -y @diffohq/diffo resolves a fresh CLI on every run. The examples below write diffo for brevity.
Every command runs from inside a git repository and is safe to re-run. Feedback lives in the review, not in any process, so an interrupted command loses nothing.
Commands at a glance
| Command | Side | What it does |
|---|---|---|
diffo | reviewer | Open (or resume) the review for this repo |
diffo <target> | reviewer | Review against a branch, or review a pull request |
diffo pr <ref> | reviewer | Review a pull request, spelled out: the same as diffo <ref> when the target is a PR |
diffo status | reviewer | Is a server watching this repo, and where |
diffo stop | reviewer | Stop this repo's server |
diffo clean | reviewer | Remove the worktrees Diffo made for pull requests |
diffo setup | reviewer | Register Diffo with the coding agents on this machine |
diffo telemetry | reviewer | See, turn off, or turn on anonymous usage data |
diffo poll | agent | Wait for the reviewer's feedback |
diffo reply | agent | Reply to a thread |
diffo comment | agent | Start a thread in the agent's own voice |
diffo layers | agent | Outline the changeset as steps to read in order |
diffo end | agent | Detach from the review |
diffo help | both | Help for the CLI, or for one command |
diffo help pr | both | Help for reviewing a pull request: the spellings, the worktree, what gh needs |
Opening a review
diffo # the working tree vs HEAD
diffo main # everything since forking from main (same as --base main)
diffo https://github.com/acme/widgets/pull/482 # a pull request; also acme/widgets#482, #482, 482
diffo pr 482 # the same, spelled out
diffo --no-open -p 4949 # a fixed port, no browser| Option | What it does |
|---|---|
--base <branch> | Review everything since forking from <branch> (merge-base). Default: the working tree vs HEAD |
-p, --port <port> | Port to serve on. Default: the port this repo last used, else the first free one |
--no-open | Don't open the browser. What an agent should always pass |
--foreground | Keep the server in this terminal (Ctrl-C stops it) instead of leaving a background one behind |
The command prints the changeset summary and the review URL, then returns immediately, leaving a background server that keeps watching after the terminal (or the agent session) that opened it is gone. Run it twice in the same repo and the second run points you at the existing review rather than starting a second one.
A background server self-stops after 30 minutes in which no browser tab and no agent poll used it. See DIFFO_IDLE_TIMEOUT_MS to change or disable that. --foreground never self-stops.
Untracked files are included, so brand-new agent output shows up as an addition rather than not at all.
Reviewer commands
diffo status
diffo status
diffo status --jsonPrints the changeset summary, the server behind it (port, pid, version), and the review URL. --json prints one object instead, either the server behind the review:
{ "running": true, "port": 4949, "pid": 51234, "version": "0.0.1", "url": "http://localhost:4949" }or, when nothing is watching this repo:
{ "running": false }Exits 0 when a server is watching this repo and 1 when none is, so it works as a shell condition.
diffo stop
Asks this repo's server to shut down cleanly, falls back to a signal if it lingers, and clears its registration. Stopping nothing is still a success.
The review itself survives: threads live in SQLite, and the next diffo picks them back up.
diffo clean
diffo clean # remove worktrees whose review is over
diffo clean --force # the ones with uncommitted changes too
diffo clean --all # every worktree Diffo madeLists every worktree Diffo made for a pull request and removes the ones whose review is over: the PR was merged or closed, the review was pruned, or the directory is gone. A worktree is kept and named when it has uncommitted changes, when its state could not be checked, or when a Diffo server is still reviewing in it; --force removes those too. A worktree git could not remove stays listed for the next run. See Reviewing a pull request.
diffo setup
Registers Diffo with the coding agents on this machine, and installs the Agent Skill that teaches them when to open a review and how to behave inside one. It never touches an agent that isn't installed, never overwrites anything that isn't Diffo's, and is safe to re-run.
One row per client, each with a status:
| Status | Meaning |
|---|---|
registered | Wired up. Restart or reload that client so it discovers Diffo |
manual | One step needs your hands; the row says which. Re-run diffo setup afterwards |
absent | That client isn't installed here |
failed | Something went wrong; the row says what |
Exits 1 if any row failed, otherwise 0.
Which clients, and how each is wired, is in Getting started, along with the manual equivalent of every row if you'd rather do it by hand.
You don't need to re-run setup after an upgrade: opening a review refreshes the skill copies it installed.
diffo telemetry
diffo telemetry [status|on|off]Anonymous usage data: two small events per review, on by default, announced in the review page before the first one is sent. status (the default) prints whether it is on, why it is off, and where the data goes. off stops reporting on this machine for good and deletes the machine's random id; on starts again with a fresh one. Usage data lists every field.
DIFFO_TELEMETRY_DISABLED=1 or DO_NOT_TRACK=1 turns it off without touching the setting, and wins over it; so does CI.
Agent commands
These are the whole agent protocol. Any agent that can run a shell command can drive the loop; the agent protocol is the full reference behind it.
All five talk to this repo's server, starting one if none is running.
diffo poll
diffo pollBlocks until the reviewer acts, then prints one JSON payload naming the threads to act on, and exits 0. Whitespace heartbeats every 15 seconds keep the connection alive; a single poll is capped at 30 minutes.
{ "status": "feedback", "kind": "threads", "threadIds": ["t-3"], "prompt": "…", "next_step": "…" }status is one of feedback (the only one that carries work), timeout, superseded (another session took the review), or ended. A feedback payload's kind is threads (individual sends), finish (the reviewer is done reading), layers (the reviewer asked for the outline; see diffo layers), or cleared (they started the review over).
Run it attended: as a tracked background task when its completion wakes the session on its own, otherwise in the foreground without ending the turn, and never detached: a payload delivered to a process nobody is reading never reaches the agent. Safe to re-run any time; delivery is at-least-once, so a poll that dies mid-write re-delivers instead of dropping feedback.
diffo reply <threadId>
diffo reply t-3 --message "Fixed: the guard now covers the empty case."
echo "$long_reply" | diffo reply t-3Posts one message into a thread, rendered as GitHub-flavored Markdown in the reviewer's UI (a ```mermaid fence draws a diagram). Thread ids come from poll payloads.
| Option | What it does |
|---|---|
-m, --message <text> | The reply. Omit it to read the message from stdin |
--more | An interim reply: a follow-up on this thread is promised, so the reviewer keeps seeing the agent at work |
--suggest-reply <one line> | Offer the reviewer their answer. It appears as ghost text in their reply box; Tab takes it, and they still press send. For a message that ends in a decision that is theirs ("want me to extract this?" → --suggest-reply "yes, extract it"), never for one that only reports |
--pr-comment <text> | Pull requests only. Attach the review comment the reviewer would leave the author: in their voice, to the author, as GitHub will show it (Markdown; a ```suggestion block when the fix is local to the anchored lines). It appears under the reply with Add as PR comment draft, Edit and Dismiss; nothing posts until the reviewer submits. The agent's evidence stays in --message. Refused off a pull request. See Reviewing a pull request |
Prints { "ok": true, "threadId": "t-3", "state": "…", "next_step": "…" }. An unknown thread id fails with exit 1. Each run posts a message, so don't re-run a reply that succeeded.
diffo comment [<file>]
diffo comment src/server/db.ts --line 88 --message "This lock ordering looks wrong to me."
diffo comment --message "Worth a second pair of eyes on the whole migration."Opens a thread as the agent, anchored to a line, a file, or, with no file, to the whole changeset. For something the agent noticed in its own work, or context that helps the read.
| Option | What it does |
|---|---|
-m, --message <text> | The comment. Omit it to read the text from stdin |
--line <n> | Anchor to a line. Needs a file argument |
--suggest-reply <one line> | Offer the reviewer their answer as ghost text in the reply box, taken with Tab. Same rule as on reply: only when the comment proposes something and the call is theirs |
--pr-comment <text> | Pull requests only. The review comment the reviewer would leave the author about this, same rules as on reply. For a finding the agent can show, never a hunch |
Prints { "ok": true, "threadId": "t-1", "next_step": "…" }.
An agent-started thread is inert until a human replies into it: it doesn't count toward the reviewer's outstanding feedback and isn't flushed by Finish review.
diffo layers
diffo layers --suggest "the parser change explains the rest" # at open: this read benefits from layers
diffo layers --json '[{"title":"Parser contract","files":["src/parse.ts"]}]'
cat outline.json | diffo layers --stdinOutlines the changeset as layers: ordered steps, each with a title, a short summary of what the step is about (a diagram when it helps), and the files that belong to it. The reviewer reads one layer at a time; files no layer lists gather in a trailing Since your review layer until the agent re-posts. Layers has the doctrine: when to suggest, how to order, what a summary may say.
Exactly one of:
| Option | What it does |
|---|---|
--suggest ["<why>"] | Flag, without writing them, that this read benefits from layers. The optional reason is one line, quoted to the reviewer next to the offer |
--json '<Layer[]>' | Post the outline. Replaces the whole list; ids are kept for titles that match, so the reviewer's place survives a re-post |
--stdin | The same payload, piped |
A layer is { title, summary?, kind?, files, decisions? }. files holds repo-relative paths, as strings or as { "path", "note" } where the note is one line on why the file is in this step. kind may only be "mechanical": the layer changes no behaviour, and its files render folded. A path:from-to string is accepted and reserved; today it means the whole file. Unknown paths are accepted and show nothing until the file lands.
decisions is optional and holds at most five { text, detail?, at? }: text one short line, detail one sentence more, and at where it lives: "path", "path:line" or "path:from-to" (head side), or a list of up to three such places when the decision spans hunks. A sixth decision, a fourth place or a malformed at fails the post the same way a bad file does. Layers has what earns a line.
A title that is empty or used twice, an empty file list, an absolute or ..-escaping path, a kind other than mechanical, or more than five decisions fails with exit 1 and names the layer at fault (layer 2 ("Callers"): needs a non-empty "files" list). Nothing is stored on a failed post.
Prints { "ok": true, "layers": 4, "next_step": "…" } after a post, and { "ok": true, "suggested": true, "next_step": "…" } after a suggestion. suggested is false when layers already exist: there is nothing left to suggest, and the next step says to re-post instead.
The reviewer's Ask the agent to outline button, and re-outline over an existing outline, reach the agent as a kind: "layers" poll payload whose prompt asks for exactly this post. A post concludes the request.
diffo end
Detaches the calling session politely. If another session is the attached agent, nothing is touched and the output says so: { "ok": false, "reason": "not-owner", "ownerPid": 51234 }. Safe to re-run.
Help and version
| Command | What it does |
|---|---|
diffo --help, -h | The full usage page |
diffo help <command> | Help for one command, including its output shape |
diffo help agent | The agent's whole protocol on one page |
diffo help pr | Reviewing a pull request: the target spellings, the worktree, and what gh needs |
diffo --version, -v | Print the version |
-h works after a command too: diffo comment -h is the same as diffo help comment.
An unrecognised command is an error with a suggestion (diffo repyl offers reply), not a silent no-op.
Environment variables
| Variable | Effect |
|---|---|
DIFFO_DB | Path to the state file. Default ~/.diffo/diffo.db |
DIFFO_IDLE_TIMEOUT_MS | Idle budget before a background server self-stops. 0 or off disables it. Default: 30 minutes for a background server, off in the foreground |
DIFFO_SERVER_LOG | Where a background server writes its log. Default ~/.diffo/logs/<repo>-<hash>.log |
DIFFO_TELEMETRY_DISABLED | Set to 1 to send no usage data. DO_NOT_TRACK=1 and CI do the same |
DIFFO_TELEMETRY_DEBUG | Set to 1 to write every usage-data payload to the server log before it is sent |
DIFFO_DAEMON | Internal. Set by the CLI on the server it spawns; don't set it yourself |
Exit codes
| Code | When |
|---|---|
0 | Success |
1 | Any failure: not a git repo, a bad flag, no server for status, an unknown thread id for reply, a failed row in setup |
Errors go to stderr as diffo: <what went wrong>. Machine-readable output (--json, and every agent command) goes to stdout on its own, so it stays pipeable into jq.
See also
- The agent protocol: the poll payloads, the intent contract, presence, and who owns a review
- Keyboard shortcuts: the review UI itself
- Getting started: install, and one setup per agent