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
FLAGMEANING
[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-syncKeep review checks local: don't tick files off as "viewed" on the pull request. GitHub only; same as forge.viewed_sync = false.
--init-configWrite 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.
doctorCheck 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, --versionPrint 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

KEYACTION
h / lFocus the file tree / the code pane (arrows work too; clicking a pane focuses it).
j / kMove the cursor in the focused pane (clicking a line also moves it there).
d / uJump 15 lines (also PageDown / PageUp).
g / GJump to top / bottom; 10G goes to line 10.
H / LPrevious / next file, skipping reviewed and generated ones.
oOpen / close a folder (also Enter, Space).
tShow / hide the file tree.
fSwitch 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

KEYACTION
[ / ]Widen / narrow the tree-sitter block scope around a change.
wPeek 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.
zCollapse / expand unchanged lines.
CFold / unfold comment blocks.
x / XCheck 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.
vVisual mode: select lines.
yCopy the line or selection.
sSend the line or selection to an AI agent pane.
cCopy the file path.
eOpen the file in your editor at the cursor's line.
bOpen 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.
BPick 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.
rRefresh.
qQuit (Esc only cancels — it never quits).

Pull requests

KEYACTION
pList the repo's open PRs / MRs; inside a PR, return to the list.
a / AComment on the line or reply to the thread / write a PR-level comment.
TFold / unfold the review thread under the cursor.
rOn a thread: toggle resolved. Elsewhere: refetch the PR.
dOn 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:

ENTRYMEANING
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 = falseNerd 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/.

FILEMEANING
language.tomlThe manifest: name, file extensions, grammar repo pinned to a commit, and the block kinds.
highlights.scmThe tree-sitter highlight query: drift's curated stack, or the grammar repo's bundled query — yours to edit either way.
injections.scmOptional: 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 build rebuilds 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.