drift docs
Everything to install, drive, and configure drift. The landing page shows what it looks like; this page is the reference.
Install
macOS & Linux — prebuilt binaries (x86_64/aarch64) land
in ~/.local/bin; override the directory with
DRIFT_INSTALL_DIR. The Linux binaries link against
glibc (2.17+) — language plugins are loaded with dlopen,
which a static binary cannot do. On musl-based distros (Alpine,
etc.), install with cargo instead:
$ curl -fsSL https://tothalex.github.io/drift/install.sh | sh
Cargo — builds from source on your machine (Rust 1.96+);
the package is drift-tui, the binary it installs is
drift. On musl distros a native build links musl
dynamically, so language plugins work (with a rustup musl
toolchain add RUSTFLAGS="-C target-feature=-crt_static"):
$ cargo install drift-tui
Windows (experimental) — download
drift-windows-x86_64.zip from the
latest release,
unzip, move drift.exe somewhere permanent, and add that
folder to your user PATH. Windows Terminal is
recommended.
Or build from source on any platform.
Updating — drift update installs the latest
release in place (--check just reports); both print
the changelog for everything between your version and the
latest. drift also
checks for a newer release on launch, at most once a day, and
mentions it in the status bar — disable under
[update]. Installs
managed by cargo or homebrew are refused with the right command
instead.
Usage
$ drift # review the current repo $ drift --base dev # compare against a different base $ drift ~/some/repo # review another repository $ drift --pr 123 # open pull request #123 right away $ drift doctor # check the CLIs drift drives: versions, auth
| FLAG | MEANING |
|---|---|
| [path] | Repository or worktree to review; defaults to the current directory. Once inside, b switches between the repo's worktrees and branches without restarting. |
| -b, --base <branch> | Base branch to compare against. Auto-detected when absent: origin/HEAD, then main, then master; an orphan branch with no history in common falls back to its own upstream. |
| --pr <number> | Open this pull request / merge request immediately (needs an authenticated gh or glab). |
| --no-viewed-sync | Keep review checks local: don't tick files off as "viewed" on the pull request. GitHub only; same as forge.viewed_sync = false. |
| --init-config | Write the fully documented default config to ~/.config/drift/config.toml and exit. |
| lang <command> | Manage language plugins: install, build, list, remove. |
| update [--check] | Update drift to the latest release, or just report one with --check. Needs curl and tar. |
| doctor | Check the external CLIs drift drives (gh, glab, herdr, tmux, cmux): versions, required minimums, auth, and what the current directory resolves to. Paste the output into bug reports. |
| -V, --version | Print the version. |
The comparison point is git merge-base <base> HEAD,
diffed against the working tree — committed work, uncommitted edits,
and untracked files all show up together, live as you edit.
Repositories are read natively (gitoxide); the git
binary is not required.
Long lines wrap at the pane edge (vim-style, hanging behind the gutter) instead of clipping. Wrapping is display-only: motions, visual mode, yanks and send-to-agent operate on real lines. In the file tree, a name clipped by a narrow pane shows its full path in a tooltip under the cursor; the review board answers a clipped commit summary or branch name the same way, and an expanded row widens the panel to fit the summaries it holds.
Generated files — lockfiles (Cargo.lock,
package-lock.json, go.sum, …), minified
bundles, protobuf and *.generated.* output — recede in
the file tree, H / L step over them (a review
never opens on one), and they don't count toward review progress.
.gitattributes has the final say:
linguist-generated marks more files,
-linguist-generated opts one back in.
Keybindings
Press ? inside the app for this list; every binding is rebindable. Motions take vim-style counts (10j).
Navigate
| KEY | ACTION |
|---|---|
| h / l | Focus the file tree / the code pane (arrows work too; clicking a pane focuses it). |
| j / k | Move the cursor in the focused pane (clicking a line also moves it there). |
| d / u | Jump 15 lines (also PageDown / PageUp). |
| g / G | Jump to top / bottom; 10G goes to line 10. |
| H / L | Previous / next file, skipping reviewed and generated ones. |
| o | Open / close a folder (also Enter, Space). |
| t | Show / hide the file tree. |
| f | Switch the tree between review groups — code, tests, docs, config, dependencies, generated, each over its own directory tree with its review progress — and plain directories. Starts grouped; see [tree]. |
| > / < | Resize the panes (or drag the divider). |
| / | Search the focused pane; n / N cycle matches — with no search active they hop between changes. |
Review
| KEY | ACTION |
|---|---|
| [ / ] | Widen / narrow the tree-sitter block scope around a change. |
| w | Peek the block under the cursor as it reads now — the code pane switches to the new side only, no deletions, new lines accented. Inside the peek [ / ] walk the enclosing blocks outward and back in; w again returns to the diff. |
| z | Collapse / expand unchanged lines. |
| C | Fold / unfold comment blocks. |
| x / X | Check a file off as reviewed / uncheck the last one. A file that changes after its check comes back unchecked, marked ↻. In a GitHub pull request this ticks the file as "viewed" there too. |
| v | Visual mode: select lines. |
| y | Copy the line or selection. |
| s | Send the line or selection to an AI agent pane. |
| c | Copy the file path. |
| e | Open the file in your editor at the cursor's line. |
| b | Open the review board: every worktree of the repo, newest first, with what each holds, how long ago it moved, and — inside herdr — the agent working in it. Press b again to flip to the repo's branches, each reviewable at its tip without a checkout. l and Enter are the same key on the board: on a row they open it into its scopes and commits and close it again, and on a scope or commit line they select it — so reviewing all of a row is its all changes line (all commits on a branch). h closes a row from anywhere inside. Reopens as you left it — the same rows open, the cursor on the line you picked. Base branch and pull requests are actions at its foot. |
Inside any picker — the board and the lists it chains into — navigation is the same vocabulary as the panes: j/k with counts (5j), g/G for the first and last line (12G goes to line 12), / to search the rows on screen with n/N stepping matches, Enter to pick, Esc or q to close. | |
| B | Pick the review scope on the current worktree directly (everything, committed only, uncommitted only, one commit) — e.g. jump straight to a commit. One commit opens on its message: a # commit message entry pinned above its files carries the full message, author and date, and the status bar shows its subject. |
| r | Refresh. |
| q | Quit (Esc only cancels — it never quits). |
Pull requests
| KEY | ACTION |
|---|---|
| p | List the repo's open PRs / MRs; inside a PR, return to the list. |
| a / A | Comment on the line or reply to the thread / write a PR-level comment. |
| T | Fold / unfold the review thread under the cursor. |
| r | On a thread: toggle resolved. Elsewhere: refetch the PR. |
| d | On your comment, pressed twice: delete it. |
Configuration
Everything lives in ~/.config/drift/config.toml
(respects $XDG_CONFIG_HOME). Generate the fully
documented default file to edit from:
$ drift --init-config
Top-level entries:
| ENTRY | MEANING |
|---|---|
| base = "main" | Default comparison branch; --base overrides, auto-detected if unset. |
| colorscheme = "onedark" | Base palette: a built-in name or a file in themes/ next to the config. |
| editor = "nvim +{line}" | Editor command for e; {file} and {line} are substituted, and the path is appended when {file} is absent. E.g. "code -g {file}:{line}". |
| icons = false | Nerd Font icons in the tree: per-language file icons, folder glyphs, one per review group, and the pinned conversation / commit entries. Opt-in — needs a patched font in the terminal, which drift can't detect; --icons turns it on for one session. |
[keys] — rebind anything
Keys take single characters ("g", "G",
"<"), named keys (enter, space,
tab, arrows, pageup/pagedown,
home/end), optionally prefixed
ctrl-. Digits and esc are reserved.
Listing an action replaces all of its default keys:
[keys] next_file = ["tab"] yank = ["y", "ctrl-c"]
[theme] — every color yours
Colors take ANSI names ("darkgray"), 256-color
indexes ("114"), or hex ("#87d787") —
UI accents and the full syntax palette alike. Colors layer in
three steps: the built-in onedark palette, then a
named colorscheme file, then [theme] overrides on
top.
# ~/.config/drift/config.toml colorscheme = "mytheme" # ~/.config/drift/themes/mytheme.toml — same keys as [theme]; # missing keys keep the built-in defaults keyword = "#fb4934" string = "#b8bb26" [rust] bracket = "#fe8019"
A [theme.<lang>] section (naming any
installed or curated language — rust,
typescript, go, …) overrides any syntax color for that language
only — the way onedark itself paints brackets orange in
TypeScript but purple in Go.
[forge] — pull-request integration
drift talks to GitHub/GitLab through the official
gh / glab CLIs and detects the forge
from the origin remote. If your self-hosted
hostname names neither "github" nor "gitlab", set it
explicitly:
[forge] kind = "gitlab" # or "github" # gh = "/path/to/gh" # binary overrides, if not on PATH # glab = "/path/to/glab" # viewed_sync = true # x also ticks the file viewed on GitHub
In a pull request, checking a file off with x also
ticks it as viewed on GitHub, and the ticks already
there come back as checks when you open it. Set
viewed_sync = false (or pass
--no-viewed-sync) to keep your checks to yourself.
GitLab exposes no per-file viewed state, so there the checks are
always local.
[agent] — send code to an AI session
[agent] # backend = "auto" # auto | herdr | tmux | cmux | off — naming one # # forces it from outside that session too # target = "claude" # pin an agent name or pane id; empty = auto-pick # submit = true # false: stage the prompt without pressing enter # # (cmux has no staging — it always submits) # template = "{input}\n\n{file}:{lines}\n```\n{code}\n```"
Template placeholders: {input} is the typed
instruction; {file} (absolute path),
{relfile} (repo-relative), {lines}
("12" or "12-18"), {start}, {end}, and
{code} describe the selection.
[board] — the review board's first axis
[board] # first = "worktrees" # or "branches" — the list b opens on
The board has two axes over the same repo: worktrees, the live working copies with their uncommitted work, and branches, reviewed at their tip whether or not anything has them checked out (committed work only — there is no working copy to read). b flips between them at any time and each keeps its own place; this setting only picks the one you start on.
[tree] — the file tree's layout
[tree] # layout = "groups" # or "directories" — the layout drift starts in
By default the tree groups the changes by what each file is: code first, then tests, docs, config, dependencies (manifests), and generated output last, each group over its own directory tree with how much of it is reviewed. The rules go by path alone, so they work for any language, and a changeset of a single kind keeps the plain tree. f flips to the plain directory tree and back at any time; this setting only picks the one you start in.
[picker] — how much screen a panel may take
[picker] # width = "80%" # ceilings, not fixed sizes # height = "80%"
The review board and the lists it chains
into (base branch, pull requests, agent targets) are sized by
what they hold; these are the most of the terminal they may
cover. Raise them and a wide terminal shows long branch names
and commit summaries in full instead of clipping them to a
tooltip; lower them to keep a panel off most of the screen.
Percentages between 10% and 100%, written "80%" or
80.
[update] — the new-release check
[update] # check = true # look for a newer release on launch (at most # # once a day) and mention it in the status bar
The check reads only where
releases/latest redirects — no API, nothing sent.
Installing is always explicit: drift update.
Language plugins
Every language is a plugin: a tree-sitter grammar fetched and compiled on your machine, plus a highlight query — no grammar is compiled into drift itself. The first time drift shows a file whose language is available but not installed, it offers it right there: press y and keep reviewing while the grammar builds in the background — highlighting and block scoping appear in place when it's ready; n declines for the session. Files without an installed language review as plain hunks.
The same works from the command line. Installing needs
git and a C compiler — the only place drift uses a
compiler (and, besides the pull-request fetch prompt,
git).
$ drift lang install rust # curated: bash, c, cpp, csharp, css, dockerfile, # go, html, java, javascript, json, lua, markdown, # php, python, ruby, rust, toml, tsx, typescript, # yaml $ drift lang install https://github.com/tree-sitter-grammars/tree-sitter-zig $ drift lang list # installed and installable $ drift lang build # rebuild grammars (after a drift upgrade) $ drift lang remove rust
For the languages drift curates deeply (rust, typescript, tsx,
javascript, python, go) the installed highlights.scm
is drift's full query stack — the grammar's bundled queries
plus hand-tuned supplements that close the gaps against
editor-grade highlighting — and it's yours to edit. Other
languages get the grammar repo's bundled query.
A plugin is a directory under
~/.config/drift/languages/<name>/; compiled
grammars are cached in ~/.cache/drift/grammars/.
| FILE | MEANING |
|---|---|
| language.toml | The manifest: name, file extensions, grammar repo pinned to a commit, and the block kinds. |
| highlights.scm | The tree-sitter highlight query: drift's curated stack, or the grammar repo's bundled query — yours to edit either way. |
| injections.scm | Optional: embedded-language regions, tree-sitter style. html ships one, so <style> and <script> bodies highlight with the css / javascript grammars when those are installed too. |
# ~/.config/drift/languages/zig/language.toml name = "zig" extensions = ["zig", "zon"] [grammar] repo = "https://github.com/tree-sitter-grammars/tree-sitter-zig" rev = "…" # commit to build; default branch if unset # path = "zig" # subdirectory, for repos hosting several grammars # symbol = "tree_sitter_zig" # exported fn; tree_sitter_<name> if unset block_kinds = ["function_declaration", "if_statement"]
block_kinds lists the grammar's node kinds that
count as reviewable blocks — what changes expand to and what
[/] widen through: functions, classes, loops.
drift lang build warns about node kinds the grammar
doesn't actually have, and validates the highlight query against
the freshly built grammar.
- Curated languages are complete manifests (and query stacks) in the repo's languages/ directory, embedded into the binary — adding one upstream is a one-file PR. URL installs scaffold the manifest and point out what to fill in.
- Every installed language's name works in [theme.<lang>] sections.
- A broken plugin never stops drift — it degrades to a startup
warning and plain hunk view for its files. After a drift upgrade
changes the tree-sitter ABI,
drift lang buildrebuilds everything.
Grammars are native code compiled into a library that drift loads — install only grammars you trust.
Pull requests
Press p (or run drift --pr 123) to browse
the repo's open pull requests (GitHub) or merge requests (GitLab) —
the same file tree, block-scoped diffs, review progress, and search
as the primary view, with the whole conversation in place: inline
threads under the exact lines they were written on, a pinned
# conversation entry for the description and PR-level
discussion, replying, resolving, and deleting without leaving the
terminal.
Setup is the forge's own CLI — install one and authenticate once:
$ gh auth login # GitHub — https://cli.github.com $ glab auth login # GitLab — https://gitlab.com/gitlab-org/cli
That is also what makes GitHub Enterprise and self-managed GitLab
work: authenticate the CLI against your host
(gh auth login --hostname git.corp.example) and drift
picks the forge from the repo's origin remote — see
[forge] for hosts naming neither.
When the PR's commits exist locally, diffs get full
syntax highlighting and block scoping. When they don't, drift
offers to fetch them: y runs git fetch origin
pull/<n>/head (GitLab:
merge-requests/<n>/head) in the background —
objects only, no branch or worktree changes — and the views fill in
when it lands; n keeps plain hunks for that PR. Nothing
is fetched without asking, and the fetch never prompts for
credentials in drift's terminal. Large pull requests open too: past GitHub's
size limit for a whole diff, drift loads it file by file, and a
single file the forge leaves out as too large says so in place of
its diff rather than passing for unchanged.
AI agents
Select lines (or sit on one), press s, type an
instruction — the prompt lands in the Claude Code (or Codex, or any
agent) pane beside drift, with the file location and the code
attached. A selection that covers a change is sent unified-diff
style — - old lines, + new lines.
This works through herdr, a
terminal workspace manager for AI coding agents — or through plain
tmux, or cmux. In herdr, drift finds
every agent pane automatically, with its live idle/working state;
in tmux and cmux, agent panes are recognized by their running
process (claude, codex, aider, …) with the same closest-first
targeting, minus the idle/working state. Auto-detection takes the
innermost session drift runs inside — herdr or tmux ahead of the
cmux surface holding them — and naming a backend
(backend = "tmux") forces it from any terminal, except
cmux, whose socket answers only its own surfaces unless you widen
its socketControlMode setting.
The review board's agent column is herdr-only
for the same reason: herdr reports each agent's session id, so drift
can read the session's own record and put the agent on the worktree
its latest turn changed — whether it moved into a worktree of its
own (.claude/worktrees/…) or never left the main
checkout and edits another by path. Subagents and workflow agents
count toward the session that launched them, and several agents in
one worktree show as the busiest plus a count
(● claude working +1). A pane's own directory never
moves with its agent, so the other backends would credit the work
to the checkout the agent was launched from.
- An agent split in drift's own tab is picked automatically — the drift-beside-claude layout never shows a picker.
- With several sessions, a picker asks (closest first, each row naming where its agent is); ↑↓ re-aim the prompt while composing, and the title follows.
- The choice sticks: later sends aim at the same session until its pane goes away. Pin one for good with agent.target.
drift drives these sessions through their own CLIs;
herdr must be 0.7.5 or newer (older releases predate the pane
commands drift sends through). drift doctor checks
every external CLI — version, required minimum, auth.
Build from source
$ cargo build --release # binary at target/release/drift $ cargo test
Git repositories are read natively (via gitoxide) — the
git binary is not required to review. Only
drift lang install/build shell out to
git and a C compiler, to fetch and compile grammar
plugins — and the pull-request view's fetch prompt, to
git fetch a PR's commits when you accept it.