
Hunk Extensions
FreeCreate and manage custom extensions for Hunk.
Free · Opens the source repo
What Hunk Extensions does
Hunk Extensions is a skill designed for developers looking to extend the functionality of Hunk, a terminal diff viewer. This skill provides a comprehensive mapping of the authoring surface for creating Hunk extensions. It allows developers to hide or reorder files, manage sidebar panes, customize file views, and define commands and key bindings, among other capabilities. The skill is particularly useful when writing, debugging, or installing Hunk extensions, enabling users to tailor the Hunk experience to their specific needs.
To create a Hunk extension, developers need to write a single TypeScript (or JSX/JS) file that exports a factory function. This function receives an API object from Hunk upon startup, allowing the extension to interact with the Hunk environment. The skill serves as a guide to the various touchpoints available in the Hunk API, detailing how to implement features like custom themes, file language support, and VCS adapters. It emphasizes the importance of consulting the provided documentation and examples to ensure best practices are followed.
The skill is not intended for live diff review sessions, as it focuses on extension development rather than direct interaction with diffs. Instead, it is a resource for developers who want to enhance Hunk's capabilities through extensions that can be loaded and executed with user permissions. By utilizing this skill, developers can create tailored solutions that improve their workflow and adapt Hunk to their specific use cases.
When to use it
Use this skill when you need to develop or customize extensions for Hunk to enhance its functionality.
When not to use it
This skill is not suitable for users looking to review diffs in real-time, as it focuses solely on extension development.
What you can build with it
Custom Sidebar Pane
Create a sidebar pane that displays additional information or controls relevant to your workflow.
Themed File Views
Implement a custom color theme for file views to enhance readability and user experience.
Alternate VCS Support
Develop an extension that adds support for a version control system not natively supported by Hunk.
How to install Hunk Extensions
View source1. Install with the skills CLI
npx skills add modem-dev/hunk/hunk-extensions --agent claude-code2. Or install it manually
Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.
Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs
Inside SKILL.md
Written by modem-devBuilding Hunk extensions
A Hunk extension is one TypeScript (or JSX/JS) file that default-exports a factory. Hunk imports it at startup and hands it an API object. No build step, no manifest required.
// ~/.config/hunk/extensions/hello.ts
import type { HunkExtensionAPI } from "hunkdiff/extension";
export default function (hunk: HunkExtensionAPI) {
hunk.on("startup", (_event, ctx) => ctx.notify("Hello"));
}
This skill is a map of the touchpoints, not a recipe. Decide what to build from the user's request; use the table below to find the call, then read the linked material before writing code.
Sources of truth — read before writing
| Source | What it answers |
|---|---|
docs/extensions.md | The authoring guide. Every call, every rule. Start here. |
src/extension-api/types.ts | The contract — exact field names, optionality, doc comments. |
examples/extensions/* | Working extensions. Copy these patterns rather than invent. |
docs/extension-architecture.md | Hunk's internals. Needed only when changing the host. |
docs/keybindings.md, docs/themes.md | Chord grammar and theme token rules that extensions inherit. |
Outside a Hunk checkout the guide is split across
https://hunk.dev/docs/extend/extensions/ (discovery, trust, config) and its
companion pages — extension-api, file-previews, vcs-adapters, custom-sidebars —
and the contract ships as node_modules/hunkdiff/dist/npm/extension/index.d.ts.
The examples, by what they demonstrate:
review-triage/— sidebar + commands + all three dialog shapes + lifecycle events + the extension event bus + auseSyncExternalStorebridge.inline-edit/— an interactive file-viewmodedrivingctx.workspacewrites; its README explains the async lifetime rules better than anything else in tree.rendered-markdown/— a file view producing host-rendered rows from parsed Markdown, and a folder extension with an npm dependency.jsx-file-view/,jsx-file-view-gallery/— the experimental fixed-height JSX row component contract.
Where extensions live
| Source | Trust |
|---|---|
--extension <path> (repeatable) | runs immediately |
[extensions] paths in user config | runs immediately |
~/.config/hunk/extensions/ (XDG-aware) | runs immediately |
.hunk/extensions/ or repo-config paths | trust prompt |
Only the repo-local group is gated. Everything else — including --extension,
even when its path points inside the repository under review — is read as
explicit user intent and executes with full user permissions, no prompt. Never
pass or suggest a path you have not read, including one copied from a
repository's own README.
A directory matches *.ts/*.tsx/*.js/*.jsx/*.mjs at its top level, plus
one level of folder extensions. A folder is an extension if it has a
package.json with {"hunk": {"extensions": ["./index.ts"]}}, or an
index.{ts,tsx,js,jsx,mjs}. Reach for a folder only when you need npm
dependencies, helper modules, or a README; a single file keeps the install to one
cp. Hunk never installs anything, so a folder extension's node_modules has to
exist on every machine that loads it — keep a repo-shared extension
dependency-free.
The id is the file stem, or the folder name for a folder extension — unless
its manifest declares several entries, in which case each entry is its own
extension named by its own stem (numeric suffix on collision). The id is the
namespace it owns: commands are <id>.<commandId>, sidebar views
<id>:<viewId>, config [extension.<id>]. Ids match
/^[A-Za-z0-9][A-Za-z0-9_-]*$/; hunk, git, jj, and sl are reserved. A
bad or duplicate id is skipped with a startup notice.
Pick the touchpoint
| To do this | Call |
|---|---|
| Add a selectable color theme | hunk.registerTheme(theme) |
| Highlight an unrecognized file extension | hunk.registerFileLanguage(ext, lang) |
Support another VCS (git/jj/sl are reserved) | hunk.registerVcsAdapter(adapter) |
| Add a navigation/list/status pane beside the review | hunk.registerSidebarView(view) |
| Present a file as something other than a raw diff | hunk.registerFileView(view) (experimental) |
| Bind a key / add an Extensions-menu entry | hunk.registerCommand(command, handler) |
| Hide, reorder, retitle files before review | hunk.transformChangeset(fn) |
| React to loads, selection, viewed files, notes, reloads | hunk.on(event, handler) |
| Coordinate with another loaded extension | hunk.events.emit / hunk.events.on |
| Read user-supplied settings | hunk.config ([extension.<id>] table) |
Branch on the API generation (currently 3) | hunk.apiVersion |
Registration is only valid while the factory runs — Hunk seals the API object afterwards.
What handlers receive
Every event, bus, command, and file-view mode handler — plus every changeset
transform — gets ctx.cwd and ctx.notify(message, type?). A file view's
matches and layout get no context at all. Beyond that:
- Event and bus handlers also get
ctx.sidebars(open/close/toggle/isOpen on any view) andctx.events.emit. - Command handlers get
ctx.sidebars,ctx.fileViews(select/toggle/isActive/ refresh/enterMode/exitMode),ctx.selection(a snapshot of file + hunk index),ctx.navigation(live, guardedselectFile/selectHunk),ctx.commands(isEnabled/executefor public semantichunk.*commands),ctx.dialogs(confirm/select/input, queued and attributed), andctx.workspace(readDocument,canWriteDocument,writeDocumentwith consent). - Sidebar components get props:
files(frozen, filtered, review order, each withhunkssummaries),selectedFileId,selectedHunkIndex,width,theme(hex tokens plus anappearanceflag — seeExtensionPaintTheme),keybindings(ask by command id, never hard-code a chord), andactions(selectFile,selectHunk,notify). - File-view
layoutgetsfile,width,signal,changes, and a lazyreadDocument(side). - File-view
modehandlers getctx.fileandctx.fileViews.onKeymust answer synchronously — its return value ("handled"/"pass"/"exit") is the routing decision, so kick off async work and report it later throughnotifyorrefresh. Escape is host-owned and never reachesonKey.
Event payloads, sidebar props, and a command's selection all hand you frozen
ExtensionDiffFile / ExtensionDiffHunk views. A changeset transform is the
exception: it receives the live changeset and is expected to return a new one.
metadata is unfrozen either way — it is the renderer's parsed diff, so pass it
through untouched.
Rules that bite
Most extension bugs are one of these:
- Registering a surface does not show it. A sidebar view starts closed unless
it declares
defaultOpen(orreplacesDefault, which starts open in place of the built-in file list). A file view never activates itself — raw diff is the default and the user picks the view from the View menu. Ship a command that toggles it and say which key, or correct code looks like it did nothing. - A rejected file-view layout silently becomes raw diff.
hunkRowsneeds one in-bounds, inclusive entry per parsed hunk at the same array index, andsourceRangesmay not overlap on a side; invalid, oversized, cancelled, and throwing layouts warn once and fall back. - Never bundle or vendor React. Hunk serves its own
reactand@opentui/*to extension files; a second copy means a second hooks dispatcher and the component fails to render. Import them normally. OpenTUI intrinsics (box,text,scrollbox) need no import. layoutis a pure derivation of(file, width). A stateful view keeps painting its first answer untilctx.fileViews.refresh(viewId)— scope it with{ fileId }when the state belongs to one file.- Handler state must live outside the component. Panes unmount when closed;
bridge module-level state into React with
useSyncExternalStoreand immutable snapshots (review-triage/index.tsxis the working version). - A reload keeps your factory and renames the files. Factories re-run only
after a trust grant or a cwd change, so module state survives — but a file's
idencodes its position in the changeset, so a reload that adds or drops a file renumbers the rest. Key durable per-file state bypath, or reconcile it onchangeset_loaded. Pick one deliberately. - Transforms must preserve
metadata(spreading a file does), keep ids unique, and return a real changeset — otherwise the transform is skipped with a warning and the previous changeset carries forward. - Chords are defaults. Users remap by command id in
[keybindings]; built-ins win conflicts, refused one chord at a time. Bind the character shift produces ("!", not"shift+1"). ctx.commandsinvokes Hunk, not other extensions. Probe withisEnabled("hunk.review.nextHunk"), then callexecute(id, { count })for an explicitly public built-in. Counts are positive whole numbers up to 10,000, applied atomically to movement; one-shot actions run once. Unknown, disabled, private, extension-owned, or stale commands returnfalse.- Repo config can set
[extension.<id>]for a globally installed extension. Treathunk.configas untrusted for anything exec-adjacent (binary paths, shell commands, module loading). ctx.workspacewrites only apply to reloadable, unstaged working-tree reviews, by reviewed file id, inside the review root, with consent. Everything else returns{ ok: false, reason }— checkcanWriteDocumentfirst.- File-view note placement is all-or-raw per file: an unbound or range-less visible note makes Hunk render the complete raw diff instead of guessing.
- Failures are contained, not sandboxed. A throwing factory is rolled back to zero registrations and a throwing handler is a warning naming the extension — containment against bugs, not against code that should not have been loaded.
- The API touches nothing outside the review. No clipboard, no filesystem, no
process surface beyond
ctx.workspace— an extension is ordinary code, so shell out for the rest. Never write to stdout: the renderer owns it. For the same reasonhunk.logis collected as diagnostics and printed nowhere;ctx.notifyis how a user hears from you. HunkExtensionUserError(detected structurally byname) buys the full treatment — message plussuggestions, no stack trace — only from a VCS adapter operation, which is where Hunk formats it for the CLI. From a command or event handler only the message survives, as a warning toast.
Verifying
Hunk's TUI needs a real terminal, and the review UI is the user's — do not
launch hunk diff/hunk show to test, and do not reach for a pipe. No
invocation applies extensions headlessly: hunk diff … | cat still starts the
app and still takes the keyboard, so it hangs holding the user's terminal.
Practical checks, in order of cost:
- Typecheck. In a checkout,
bun run typecheckcoversexamples/extensions/**via thehunkdiff/extensionpath mapping. Standalone, addhunkdiffas a dev dependency and runtsc --noEmit; for a.tsxextension also addreact,@types/react(React ships no declarations of its own),@opentui/core, and@opentui/reactas dev dependencies and set"jsx": "react-jsx"with"jsxImportSource": "@opentui/react", or every<box>and<text>is an untyped intrinsic. Types only — shipping those packages is the second-React bug. - Unit-test the logic. When parsing, matching, or formatting is worth
testing, put it in helper modules with plain
bun testcoverage. - PTY integration. In a checkout,
test/pty/extensions-integration.test.tslaunches Hunk over a PTY with--extension <path>and asserts on rendered snapshots; extend it viatest/pty/harness.tsand runbun run test:integration. - Hand it to the user to run:
hunk diff --extension ./my-ext.--extensionloads immediately with no trust prompt, so it is the iteration path. Ask them what the footer notices and toasts said. - Triage with
--no-extensionsto confirm a symptom belongs to an extension (bundled VCS backends and the built-in sidebar stay loaded either way).
If it does not load
- No startup notice at all → a successful load is silent, so either it loaded and
nothing opened it, or discovery never saw the file. Check the directory, the
entry suffix, or the folder's
package.jsonhunk.extensionspaths. - Notice naming the extension → id rejected (reserved, malformed, or already claimed), import failure, missing default export, or a throwing factory.
- Repo-local extension silently absent → the trust prompt was dismissed or denied;
decisions are stored per repo root in
~/.config/hunk/state.json. - Sidebar pane closes with a toast → the component threw; a second React copy is the usual cause.
- Sidebar or file view never appears → nothing opened it (no
defaultOpen, no command),matchesreturned false, or the layout was rejected. - Command never fires → its chord lost to a built-in or an earlier extension (a
warning says so); it is still reachable from the Extensions menu and
bindable by
<id>.<commandId>.
Changing Hunk itself
Only when the work is in the hunk repo rather than in a user extension:
- Shipped VCS backends and the built-in sidebar are bundled extensions in
src/extensions/default/, registering through the same public API. That dogfooding is deliberate — if the public contract cannot express something, that is a real gap, not a reason for a private path.default/vcs/loads from VCS adapter resolution and must stay renderer-free. src/extension-api/types.tsmust stay import-free; declaration emission publishes whatever it reaches, andscripts/check-pack.tsfails the pack otherwise. Shapes shared with internal code are declared there and re-exported inward.- New API surface means updating
docs/extensions.md(its examples are typechecked as consumer code), the matching hand-written page underwebsite/src/content/docs/docs/extend/(onlycli.mdandconfig.mdare generated),docs/extension-architecture.mdif ownership moves, and a changeset. AGENTS.mdanddocs/extension-architecture.mdown the rest of these rules.
Frequently asked questions about Hunk Extensions
Similar skills
Spring Boot Testing
Master testing techniques for Spring Boot 4 applications.
GitHub Issues
Manage GitHub issues efficiently with MCP tools.
Geofeed Tuner
Optimize your IP geolocation feeds in CSV format.
Batch Files
Master Windows batch scripting for automation and task management.
Adobe Illustrator Scripting
Automate your Illustrator workflows with ExtendScript.
Plugin Structure
Create and organize Claude Code plugins effectively.
