Extensions
A Hunk extension is one TypeScript (or JavaScript) file that default-exports a function. Hunk imports it at startup and hands it an API object. No build step is required.
import type { HunkExtensionAPI } from "hunkdiff/extension";
export default function (hunk: HunkExtensionAPI) { hunk.on("startup", (_event, ctx) => { ctx.notify("Hello from my extension"); });}The API is experimental: hunkdiff/extension may change in breaking ways between minor releases while it stabilizes. Breaking changes are called out in release notes, and hunk.apiVersion identifies the surface an extension was written against.
What an extension can register is covered by the companion pages: the extension API, file previews, VCS adapters, and custom panes.
Writing one with a coding agent? hunk skill path hunk-extensions prints a bundled skill that maps these touchpoints for agents, the way hunk skill path does for reviewing.
Where Hunk looks
Section titled “Where Hunk looks”| Group | Source | Runs |
|---|---|---|
| 1 | --extension <path> (repeatable) | immediately |
| 2 | [extensions] paths in your user config | immediately |
| 3 | ~/.config/hunk/extensions/ | immediately |
| 4 | .hunk/extensions/ in the repo under review | after trust |
| 4 | [extensions] paths in the repo .hunk/config.toml | after trust |
- Groups load in order; within a group, entries sort alphabetically by resolved path. The first occurrence of a path wins.
- The two repo-local sources are one group: one trust decision, one sort order.
- A directory source matches
*.ts,*.tsx,*.js,*.jsx,*.mjsdirectly inside it, plus one level of folder extensions. --no-extensionsdisables user extensions for one run; nothing on disk is read.[extensions] disabledand--disable-extension <id>disable one exact extension before its module or factory runs;--enable-extension <id>overrides a configured disable for one run.--extensionis explicit intent: it loads immediately, without a trust prompt, even from inside the reviewed repo — so never pass a path you have not read.
Folder extensions
Section titled “Folder extensions”A folder is an extension if its package.json declares entries under the hunk field, or failing that if it has an index.{ts,tsx,js,jsx,mjs} (in that preference order):
~/.config/hunk/extensions/my-ext/ package.json # {"hunk": {"extensions": ["./src/index.ts"]}} node_modules/ # bun install / npm install, right here src/ index.ts # the declared entry helper.ts- Manifest paths resolve against the folder and may list several entries; each loads as its own extension, in manifest order.
- The manifest is a real
package.json, so a folder extension can depend on npm packages installed into its ownnode_modules. - Pointing
--extensionor[extensions] pathsat a directory works either way: a folder extension loads as one extension; any other directory is scanned as a directory of extensions.
Extension ids
Section titled “Extension ids”The id is the file stem, or the folder name for <name>/index.ts and single-entry manifests. It is the key for everything the extension owns:
- config:
[extension.<id>] - commands:
<id>.<commandId> - panes:
<id>:<paneId> - file previews:
<id>:<viewId>
Ids start with a letter or digit, then letters, digits, -, or _. hunk, git, jj, and sl are reserved. An invalid id — or a second source offering an already-loaded id — is skipped with a startup notice.
Installing shared extensions
Section titled “Installing shared extensions”Extensions are shared as Git repositories or npm packages. hunk extension install installs either source under ~/.config/hunk/extensions/installed/, validates its entries, and records the requested source and resolved commit or package version:
hunk extension install acme/hunk-word-diff # GitHub shorthandhunk extension install acme/hunk-word-diff@v1.2.0 # pin a tag, branch, or commithunk extension install git:codeberg.org/acme/ext # any host; https:// is assumedhunk extension install ~/dev/hunk-word-diff # a local checkout, for testinghunk extension install npm:@acme/hunk-word-diffhunk extension install npm:@acme/hunk-word-diff@1.2.0hunk extension install 'npm:@acme/hunk-word-diff@^1.2.0'hunk extension install npm:@acme/hunk-word-diff@nexthunk extension listshows every managed install with its version, commit, and source.hunk extension update [name]refreshes one install (or all of them) from its recorded source. Git refs and exact npm versions stay pinned; npm ranges and tags are re-resolved.hunk extension remove <name>deletes the install and its record. Hand-copied extensions in~/.config/hunk/extensions/are never touched.
Installing is the consent step: extensions run with your full user permissions, so a fresh install asks for confirmation (or takes --yes) after naming the source. Only install sources you trust. Managed installs then load through the global group above — same precedence, no further prompts.
npm installs require Node.js/npm on PATH, honor registry/auth configuration from the launch project's .npmrc, user/global configuration, and environment, and isolate each package's dependencies. Project credentials are forwarded during acquisition, not saved into the managed install. Hunk forces local managed installation even when npm is configured globally. Hoisted helpers receive the same host APIs and React instance as the entry. Lifecycle scripts and automatic peer installation are disabled. If npm attempts a hook despite suppression, Hunk blocks it and fails the staged install. Ship ready-to-load files; packages requiring install-time builds are not supported automatically.
Use npm:<package> from list to update or remove npm packages, for example hunk extension update npm:@acme/hunk-word-diff. npm install directories and single-entry extension IDs contain a readable package prefix plus its SHA-256 digest; this keeps scoped names distinct and IDs stable across versions. Multi-entry packages retain entry-derived IDs. Failed installs or updates do not replace the previous working directory.
Community extensions are listed at hunk.dev/extensions, and the full tail is the hunk-extension topic on GitHub.
Publishing an extension
Section titled “Publishing an extension”A publishable extension repository is the folder-extension layout at the repository root — package.json with a hunk field (or an index.* entry), code, README. To share one:
- Fill in
package.json'sname,version, anddescription, and declare"hunk": {"apiVersion": N}if you rely on recent API surface — an older Hunk then refuses to load it with a clear message instead of failing partway through. - Keep
dependenciesreal: they are installed into the extension's ownnode_modulesat install time.react,@opentui/*, andhunkdiff/extensioncome from the host at runtime and belong indevDependencies. - Tag releases so users can pin with
@v1.2.0. - Push to any git host and add the
hunk-extensionGitHub topic, then open a pull request to list it on hunk.dev/extensions.
To publish on npm, explicitly declare hunk.extensions, ensure every entry is a file inside the package, check shipped files with npm pack --dry-run, and run npm publish. Host-provided packages belong in peer or development dependencies, not runtime dependencies.
Test the exact layout users will get with hunk extension install /path/to/checkout, or load it for one run with hunk diff --extension /path/to/checkout.
Bundled extensions
Section titled “Bundled extensions”Hunk's Git, Jujutsu, Sapling, hunk gh, file-navigation, and content-search features use the same public extension API. Bundled extensions differ from yours in three ways:
- compiled into Hunk and loaded by the lifecycle that owns each capability
- implicitly trusted, with no
[extension.<id>]config table - still loaded under
--no-extensionsand[extensions] enabled = false— those switches triage extensions you installed; selectable bundled capabilities use host-owned ids such ashunk.gh
Extensions run with your full user permissions, and reviewing a repository must never execute code that came with it. So the repo-local sources stay inert until you approve them, once per repository:
Run this repository's extensions?
This repository contains extensions in .hunk/extensions. Extensions run with your user permissions.
enter/t trust · esc not now · n neverTrust records the decision and reloads the session; not now asks again next time; never stops the offers. The prompt is a dialog over the review stream, not a gate in front of it — dismiss it and keep reviewing.
Decisions are stored per repository root in $XDG_STATE_HOME/hunk/state.json (by default ~/.local/state/hunk/state.json), keyed by path (the VS Code workspace-trust model). A different repository later occupying a trusted path inherits the decision; clear the entry if that matters for a path you reuse.
Failure isolation
Section titled “Failure isolation”A broken extension is contained, not fatal: a failed import, missing default export, or throwing factory is skipped and rolled back with a startup notice; a handler or transform that throws later becomes a warning naming the extension. Event handlers receive frozen changeset copies, so accidental mutation throws instead of corrupting the review.
Extensions run with your shell permissions. For reviewed files, prefer ctx.workspace; writes require consent and identify the extension and file.
CLI flags and config
Section titled “CLI flags and config”hunk diff --extension ./path/to/entry.ts # load one entry file for a review (repeatable)hunk diff --extension ./my-ext # a folder extension: loads ./my-ext/index.tshunk --extension ./my-ext cli-tools status # run an extension-provided top-level commandhunk gh pr 123 # bundled GitHub review commandhunk --no-extensions cli-tools status # hard-disable lookup and importinghunk diff --no-extensions # disable user extensions for this reviewhunk --disable-extension hunk.gh gh pr 123 # disable one extension for this runhunk --enable-extension hunk.gh gh pr 123 # override config for this run# ~/.config/hunk/config.toml or .hunk/config.toml[extensions]enabled = true # false disables loading for this layerpaths = ["~/dev/hunk-ext/index.ts"] # extra entry files or directoriesdisabled = ["hunk.gh", "my-extension"] # exact selection ids
[extension.my-extension] # opaque payload handed to that extensionsome_key = "some value"[extensions] enabled layers like every other option (repo config overrides user config); --no-extensions is a hard off switch no config layer can re-enable. User and repository disabled lists combine as a union, while --enable-extension is the only one-run override. Disabled user extensions are filtered before import and trust prompting. Extension-provided CLI trees use registerCliCommand; bare help stays static, while hunk <extension-command> --help belongs to the extension. The dependency-free bundled @hunk/gh extension demonstrates direct HTTP preprocessing, cancellation, temporary input ownership, and one-time delegation. [extension.<id>] tables pass through to the extension uninterpreted — see hunk.config for the merge rules and their caveats.
A complete example
Section titled “A complete example”Collapse lockfiles and generated output out of every review, and say how many files were hidden.
import type { HunkExtensionAPI } from "hunkdiff/extension";
/** Match one path against a `*`-only glob, anchored at both ends. */function matchesPattern(path: string, pattern: string) { const source = pattern .split("*") .map((part) => part.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&")) .join(".*"); return new RegExp(`^${source}$`).test(path);}
export default function (hunk: HunkExtensionAPI) { const patterns = (hunk.config.patterns as string[] | undefined) ?? [ "*.lock", "*-lock.json", "dist/*", ];
hunk.transformChangeset((changeset, ctx) => { const kept = changeset.files.filter( (file) => !patterns.some((pattern) => matchesPattern(file.path, pattern)), );
const hidden = changeset.files.length - kept.length; if (hidden > 0) { ctx.notify(`Collapsed ${hidden} generated ${hidden === 1 ? "file" : "files"}`); }
return { ...changeset, files: kept }; });}Configure it without touching the code:
[extension.collapse-generated]patterns = ["*.lock", "bun.lockb", "generated/*"]Try it against the working tree without installing it:
hunk diff --extension ./collapse-generated.tsContinue with the extension API for everything the API object offers.