Hunk is a review-first terminal diff viewer for agent-authored changesets, built on OpenTUI and Pierre diffs.
- multi-file review stream with sidebar navigation
- inline AI and agent annotations beside the code
- split, stack, and responsive auto layouts
- watch mode for auto-reloading file and Git-backed reviews
- keyboard, mouse, pager, and Git difftool support
Split view with sidebar and inline AI notes |
Stacked view and mouse-selectable menus |
npm i -g hunkdiffOr with Homebrew:
brew install hunkNote
If you previously installed hunk via modem-dev/tap, be sure to uninstall it first with brew uninstall modem-dev/tap/hunk.
Requirements:
- Node.js 18+
- macOS, Linux, or Windows
- Git recommended for most workflows
Nix users can use the
defaultpackage exported inflake.nixinstead. See nix/README.md for details.
hunk # show help
hunk --version # print the installed versionHunk mirrors Git's diff-style commands, but opens the changeset in a review UI instead of plain text.
hunk diff # review current repo changes, including untracked files
hunk diff --watch # auto-reload as the working tree changes
hunk show # review the latest commit
hunk show HEAD~1 # review an earlier commitHunk auto-detects Jujutsu and Sapling checkouts, so hunk diff [revset] and hunk show [revset] use native revsets inside jj or Sapling workspaces. To override VCS detection, set vcs = "git" or vcs = "jj" or vcs = "sl" in config.
hunk diff before.ts after.ts # compare two files directly
hunk diff before.ts after.ts --watch # auto-reload when either file changes
git diff --no-color | hunk patch - # review a patch from stdinWatch mode remains continuous. Direct-file and Git-backed reviews normally use filesystem observation to refresh promptly, with periodic polling retained as a fallback for missed events or unavailable watchers. Jujutsu and Sapling reviews currently use polling rather than filesystem observation.
- Open Hunk in another terminal with
hunk difforhunk show. - Tell your agent to add the skill file returned by
hunk skill path. - Ask your agent to use the skill against the live Hunk session.
A good generic prompt is:
Load the Hunk skill and use it for this review. Run `hunk skill path` to get the skill path.
For the full live-session and --agent-context workflow guide, see docs/agent-workflows.md. Experimental rich STML note bodies require starting the review with --experimental; plain agent notes remain the default.
| Capability | hunk | lumen | difftastic | delta | diff-so-fancy | diff |
|---|---|---|---|---|---|---|
| Review-first interactive UI | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Multi-file review stream + sidebar | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Inline agent / AI annotations | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Responsive auto split/stack layout | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Mouse support inside the viewer | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Runtime view toggles | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Syntax highlighting | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
| Structural diffing | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
| Pager-compatible mode | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ |
Hunk is optimized for reviewing a full changeset interactively.
You can persist preferences to a config file:
~/.config/hunk/config.toml.hunk/config.toml
Example:
theme = "github-dark-default" # any built-in theme id, auto, or custom
mode = "auto" # auto, split, stack
vcs = "git" # git, jj, sl
watch = false
exclude_untracked = false
line_numbers = true
tab_width = 4 # tab stops, 1-16
wrap_lines = false
menu_bar = true
agent_notes = false
prompt_save_view_preferences = true
transparent_background = falsetheme = "auto" and --theme auto query the terminal background at startup, choose github-light-default for light backgrounds and github-dark-default for dark backgrounds, and fall back to github-dark-default if the terminal does not answer.
Older theme ids such as graphite and paper remain accepted as compatibility aliases.
exclude_untracked affects Git/Sapling working-tree hunk diff sessions only.
tab_width controls source-code tab stops and can be overridden with -x4 or --tab-width 4.
prompt_save_view_preferences = false disables the quit prompt for saving changed view preferences.
transparent_background can also be written as transparentBackground.
Custom themes can inherit from any built-in theme and override only the colors you care about:
theme = "custom"
[custom_theme]
base = "catppuccin-mocha"
label = "My Theme"
accent = "#7fd1ff"
panel = "#10161d"
noteBorder = "#c49bff"
[custom_theme.syntax_scopes]
"comment" = "#6e85a7"
"punctuation.definition.comment" = "#6e85a7"
"keyword.operator" = "#7fd1ff"
"entity.name.function" = "#8ed4ff"Define as many themes as you like by giving each one its own [themes.<id>] table, using the same keys. The table id is the name you select with theme = "<id>" or --theme <id>:
theme = "ocean"
[themes.ocean]
base = "nord"
label = "Ocean"
accent = "#7fd1ff"
[themes.ocean.syntax_scopes]
"keyword.operator" = "#7fd1ff"
[themes.paper-review]
base = "github-light-default"
accent = "#0969da"Theme ids must be lowercase words separated by - or _, and cannot reuse a built-in theme id. Ids that break those rules are skipped with a startup notice instead of failing the session. [custom_theme] is the theme with id custom, so it takes precedence over a [themes.custom] table. Themes appear in the selector after the built-in themes, in the order you declare them, and a repo .hunk/config.toml overrides your user config table by table for the same id.
syntax_scopes uses Shiki/TextMate scope selectors directly, so matching and precedence follow Shiki's theme rules without a Hunk-specific translation layer. Quote selectors containing dots. Declaration order is preserved; later rules win when matching selectors have equal specificity, while a more-specific base-theme selector beats a broader override. Add the grammar-specific selector when that happens. All custom theme colors must use #rrggbb hex values.
The former [custom_theme.syntax] role table is deprecated but temporarily translated into approximate scopes for compatibility. Both tables can coexist while migrating, and an exact syntax_scopes entry overrides a translated entry with the same selector. Because semantic roles have no one-to-one TextMate mapping, migrate when practical: for example, replace comment = "#ffffff" with both "comment" = "#ffffff" and "punctuation.definition.comment" = "#ffffff" under [custom_theme.syntax_scopes], adding language-specific selectors when a grammar uses more specific scopes. The compatibility table will be removed in the next major release.
Press t in the app, or choose View -> Themes…, to open the theme selector.
Set Hunk as your Git pager so git diff and git show open in Hunk automatically:
Note
Untracked files are auto-included only for Hunk's own hunk diff working-tree loader. If you open git diff through hunk pager, Git still decides the patch contents, so untracked files will not appear there.
git config --global core.pager "hunk pager"Or in your Git config:
[core]
pager = hunk pagerIf you want to keep Git's default pager and add opt-in aliases instead:
git config --global alias.hdiff "-c core.pager=\"hunk pager\" diff"
git config --global alias.hshow "-c core.pager=\"hunk pager\" show"To use Hunk as jj's pager, run jj config edit --user and update:
[ui]
pager = ["hunk", "pager"]
diff-formatter = ":git"To use Hunk as Sapling's pager, run sl config -u and update:
[pager]
pager = hunk pagerThe extension API is experimental and may change in breaking ways between minor releases while it stabilizes; breaking changes are called out in release notes.
Hunk loads plain TypeScript extensions from ~/.config/hunk/extensions/, from a
repository's .hunk/extensions/ (after you explicitly trust that repository),
and from --extension <path> for development. --no-extensions turns those off
for one run; Hunk's own bundled backends (Git, Jujutsu, and Sapling) stay loaded.
A Phase 1 extension can contribute themes and file-extension → language mappings, add a VCS backend, rewrite the changeset before review (collapse lockfiles, reorder files by review priority), react to lifecycle events, and show transient messages:
// ~/.config/hunk/extensions/collapse-lockfiles.ts
import type { HunkExtensionAPI } from "hunkdiff/extension";
export default function (hunk: HunkExtensionAPI) {
hunk.transformChangeset((changeset, ctx) => {
const files = changeset.files.filter((file) => !file.path.endsWith(".lock"));
ctx.notify(`Collapsed ${changeset.files.length - files.length} lockfiles`);
return { ...changeset, files };
});
}See docs/extensions.md for the full API, the trust model,
and the [extensions] / [extension.<id>] config reference.
Hunk also publishes HunkDiffView and lower-level primitives from hunkdiff/opentui for embedding the same diff renderer in your own OpenTUI app.
See docs/opentui-component.md for install, API, and runnable examples.
Ready-to-run demo diffs live in examples/.
Each example includes the exact command to run from the repository root.
💬 Chat with users/contributors on the Modem Discord server
For source setup, tests, packaging checks, and repo architecture, see CONTRIBUTING.md.
Sponsored by Modem.

