Staging a subset of your changes is a routine part of making clean commits. Git's built-in tool for it is git add -p — you walk through each hunk and answer y/n/s. That works at a keyboard, but it is interactive by design: you cannot drive it from a shell script, a Makefile, or an automated coding agent that needs to stage a precise set of changes without a human at the prompt.
The traditional non-interactive answer is filterdiff from the patchutils suite:
git diff path | filterdiff --hunks=1,3 | git apply --cached
filterdiff selects whole hunks by their position in the diff. The problem: a single hunk often contains several unrelated change runs separated by a few context lines. If you only want one of them, filterdiff cannot help — the whole hunk is in or out.
hunkpick
hunkpick is a small Rust CLI that fills that gap. It is a pure stdin → stdout filter: it reads a unified diff, emits the subset you asked for, and leaves applying it to you (git apply --cached). It never calls git to produce the diff, so it works with any diff source — git, Mercurial, SVN, or plain diff -u.
What it adds over filterdiff:
-
Auto-split into sub-hunks. Each hunk is decomposed into minimal sub-hunks, one per contiguous run of
+/-lines, each addressable by a stable per-file index. -
Per-file addressing. In a multi-file diff selectors are prefixed with the path (
path:1,3,path:*) so they stay unambiguous; for a single-file diff the prefix is optional and you can write the bare1,3used in the examples below. -
Content ids. Every sub-hunk carries a
@<id>derived from the file path and its changed lines only — not the context or the@@line numbers. The id survives a re-diff even when staging a neighbour renumbers indices or rewrites surrounding context, so an agent can capture it once and keep using it across a staging loop. -
Machine-readable listing.
list --jsongives structured output for tooling. -
Built-in verification. The result diff is checked for internal consistency by default;
--verify-result-diff-gitadditionally runsgit apply --check. - A single cross-platform binary, including Windows — no Unix toolchain to install.
List, then select
# See the addressable sub-hunks
git diff src/main.rs | hunkpick list
src/main.rs
[1] 6860edc905028034 @@ -1,5 +1,5 @@ +1 -1 - let a = 0;
[2] 5bb69992111224fd @@ -6,5 +6,5 @@ +1 -1 - let d = 0;
[3] 369095db02d2feec @@ -11,3 +11,3 @@ +1 -1 - let h = 0;
Each [n] is a per-file index; the @<id> after it is the content id (below). The preview is the first changed line of the sub-hunk.
# Stage sub-hunks 1 and 3, skipping the middle one
git diff src/main.rs | hunkpick select 1,3 | git apply --cached
Sub-hunks 1 and 3 are now staged; sub-hunk 2 is left in the working tree — exactly the kind of split filterdiff cannot express, because all three live in one hunk.
Splitting one file's changes into several commits
The content id is what makes an automated staging loop reliable. Bare indices renumber as you stage, but a @<id> stays valid across the re-diff — capture the listing once and keep using the ids:
# 1. Capture the ids once.
git diff src/indicator.js | hunkpick list
src/indicator.js
[1] 3a02b1b8b5f9bd02 @@ -1,3 +1,3 @@ +1 -1 - const a = 0;
[2] 0759ce5f8024a473 @@ -4,2 +4,2 @@ +1 -1 - const b = 0;
[3] 31aa062fa4795c80 @@ -6,2 +6,2 @@ +1 -1 - const c = 0;
[4] 0d33a7656bae5ea9 @@ -8,3 +8,3 @@ +1 -1 - const d = 0;
# 2. Stage and commit each group by @id, re-running git diff each round.
git diff src/indicator.js | hunkpick select @3a02b1b8b5f9bd02 | git apply --cached
git commit -m "fix: ..."
git diff src/indicator.js | hunkpick select @0759ce5f8024a473 @31aa062fa4795c80 | git apply --cached
git commit -m "feat: ..."
# 3. Whatever is left is the last group.
git diff src/indicator.js | hunkpick select '*' | git apply --cached
git commit -m "chore: ..."
Three commits: fix gets sub-hunk 1, feat gets 2 and 3, and chore gets the remaining 4 via *. Between steps the bare indices would renumber, but each @<id> stays valid across the re-diff, so the listing captured once keeps working.
hunkpick vs filterdiff
| Capability | filterdiff | hunkpick |
|---|---|---|
| Select whole hunks | ✅ | ✅ |
| Works with any diff source | ✅ | ✅ |
| Address sub-hunks by per-file index | ❌ | ✅ |
| Auto-split hunks at change-run boundaries | ❌ | ✅ |
| Stable content ids across re-diffs | ❌ | ✅ |
| Machine-readable listing (JSON) | ❌ | ✅ |
| Built-in result verification | ❌ | ✅ |
| Split an addition-only block by line range | ❌ | ✅ |
Install
cargo install hunkpick
# or a prebuilt binary:
cargo binstall hunkpick
Links: GitHub · crates.io · docs.rs.
Disclosure: this project was developed with the help of an AI coding assistant, and this article was drafted with LLM assistance and edited by the author.