git-hunk
Non-interactive, programmatic alternative to git add -p.
Every staged or unstaged Hunk gets a durable ID, so you can inspect, filter, and stage changes without interactive prompts. Duplicate Hunks get unique Conditional IDs.
Comparison • Install • For AI agents • Quick start • Usage • JSON output
Why?
git add -p asks a question for every hunk, so these callers cannot use it:
- AI agents (Claude Code, Codex, and others) that split changes into logical commits
- Scripts and CI jobs that organize commits
- Editor integrations that want hunk-level staging without running a TUI
git-hunk gives each staged or unstaged Hunk a durable ID and takes those IDs
as arguments to stage, unstage, discard, and commit.
Comparison
| Interactive | Programmatic | Hunk IDs | Line-level control | JSON output | |
|---|---|---|---|---|---|
git add -p |
Yes | No | No | Yes | No |
git add <file> |
No | Yes | No | No | No |
git-hunk |
No | Yes | Yes | Yes | Yes |
Eval
One agent (Claude Code 2.1.302, claude-sonnet-5-5, reasoning effort high)
attempted the same eight tasks from identical repository state, five times per
variant. Each task asks the agent to organize a dirty working tree into correct,
focused commits, once following git-hunk's bundled skills and once restricted to
bare Git. The
checked-in eval harness
grades the exact resulting repository state: commit partition and order, final
tree, index, and leftovers. This table records the qualifying run. just eval
reruns the protocol and prints a table in the same format.
| Task | git-hunk | bare Git |
|---|---|---|
| split_refactor_vs_feature | PASS 5/5 · 2c [2-3] · 3t [3-4] | PASS 5/5 · 3c [2-3] · 4t [3-4] |
| separate_mixed_hunks | PASS 5/5 · 2c · 3t | PASS 5/5 · 3c [2-3] · 4t [3-4] |
| drop_debug_lines | PASS 5/5 · 2c · 3t | PASS 5/5 · 3c [2-3] · 4t [3-4] |
| protect_unrelated_work | PASS 5/5 · 2c · 3t | PASS 5/5 · 2c · 3t |
| split_single_hunk | PASS 5/5 · 2c [2-3] · 3t [3-4] | PASS 5/5 · 3c [2-3] · 4t [3-4] |
| separate_formatter_noise | PASS 5/5 · 2c [2-3] · 3t [3-4] | MIXED 3/5 partition · 3c [2-4] · 4t [3-5] |
| pick_duplicate_hunk | PASS 5/5 · 2c · 3t | MIXED 3/5 partition · 3c [2-3] · 4t [3-4] |
| commit_parseable_subset | PASS 5/5 · 2c [2-3] · 3t [3-4] | PASS 5/5 · 3c [2-3] · 4t [3-4] |
| total | 8/8 · 16c [16-20] · 24t [24-28] | 6/8 (2 mixed) · 23c [16-24] · 31t [24-32] |
c is tool calls and t is turns. A cell reports the median of its five
repeats, with the observed range in brackets when the repeats disagreed, and the
pass column counts passing repeats. MIXED j/5 means the variant passed j of
its five repeats. partition names the failure: the commits made do not match
the required change groups.
Every bare Git failure made no commit. In each one, the agent passed answers to
git add -p through a here-string. The eval's permission settings
(Bash(git:*) in dontAsk mode) denied that command, and the agent stopped to
ask for permission. Passing runs split the same changes by piping the answers
or by writing a patch for git apply --cached. git-hunk takes Hunk IDs and
patterns as arguments, so it never needs stdin.
The table omits cost, because bare Git runs second in each pair and partly reads
the prompt cache the git-hunk run warmed
(ADR 0005).
Five samples per task variant, dated 2026-10-17 at commit 1112db7.
Install
Requires Git 2.28 or later. git-hunk forces canonical diff paths with
git diff --no-relative, which earlier versions of Git do not accept.
pip install git-hunk
Or with uv:
uv tool install git-hunk
Verify it works:
git-hunk --version
For AI agents
The CLI ships its own usage guide, so an agent (Claude Code, Codex, and others) can load it on demand. The guide always matches the installed version:
git-hunk skills get core
core covers the commands. The optional logical-commits skill covers how to
group hunks into commits and order them. A project with its own commit
conventions can load core alone:
git-hunk skills # list available skills
git-hunk skills get core logical-commits # load both
Before committing the working tree, an agent can load the skills and the tree's hunks in one call and plan from that output:
git-hunk skills get core logical-commits && git-hunk list && git-hunk show
git-hunk --help also tells agents to start with git-hunk skills get core.
Quick start
# See all hunks across staged, unstaged, and untracked files
git-hunk list
# Show the diff for a specific hunk
git-hunk show d161935
# Stage specific hunks, then commit
git-hunk stage d161935 a3f82c1
git commit -m "feat: add validation for user input"
# Stage the remaining hunks
git-hunk stage e7b4012
git commit -m "fix: handle empty response in API client"
Usage
Repository paths
A Repository path is relative to the worktree root, uses /, and means the
same file from any directory you run git-hunk in. Every path in output and every
file operand for list, stage, unstage, discard, and commit is a
Repository path. git-hunk normalizes a leading ./ and internal ..
components, and rejects absolute paths and paths that leave the worktree.
A file operand selects one exact changed file. git-hunk does not expand
directories, globs, or Git pathspec syntax. Quote operands that contain shell
metacharacters so the shell passes them unchanged. For example, from sub/,
same.txt selects the file at the worktree root, and sub/same.txt selects the
file inside sub/. show takes Hunk IDs only.
Unsupported repository states
git-hunk rejects detected rename, copy, and unmerged index states before it writes output or changes the repository, so it never prints partial JSON, a partial inventory, or a false clean result, and never applies half a change. Resolve an unmerged index with Git, then retry. Rename and copy support is tracked in #53.
Hunk IDs
A canonical Hunk ID is a full SHA-256 value. JSON returns it in full. Human output shows the shortest unambiguous prefix of at least seven characters, and commands accept any unambiguous prefix in either case. git-hunk computes IDs from the combined staged and unstaged inventory, even when a status filter shows only one side.
An Unchanged Hunk keeps its ID when it moves completely between staged and unstaged state or when other complete Hunks move. A partial-line operation creates new Hunks with new IDs.
Hunks with the same Repository path and patch content form a Duplicate Hunk
group. Each member gets a unique Conditional Hunk ID, shown with a
conditional label in human output and "id_stability": "conditional" in JSON.
The ID can change when its Duplicate Hunk group changes. After a partial-line
operation or an operation on a Conditional Hunk ID, address anything remaining
by Repository path, which is ID-independent, or run git-hunk list again for
the new IDs.
List hunks
git-hunk list # all hunks (unstaged + staged + untracked)
git-hunk list --unstaged # unstaged hunks only
git-hunk list --staged # staged hunks only
git-hunk list src/foo.py src/bar.py # specific files
git-hunk list --json # JSON output for scripting
Show hunks
git-hunk show # show all hunks (staged + unstaged)
git-hunk show d161935 # show a single hunk
git-hunk show d161935 a3f82c1 # show multiple hunks
git-hunk show --staged # show all staged hunks
git-hunk show --unstaged # show all unstaged hunks
Stage, unstage, discard
git-hunk stage d161935 # stage a hunk
git-hunk stage d161935 a3f82c1 # stage multiple hunks
git-hunk stage d161935 -l 3,5-7 # stage specific lines only
git-hunk stage d161935 --exclude-matching debug # stage all but lines containing "debug"
git-hunk stage d161935 --include-matching xfail # stage only lines containing "xfail"
git-hunk unstage d161935 # move back to working tree
git-hunk unstage d161935 -l 3,5-7 # unstage specific lines only
git-hunk discard d161935 # restore from the index
git-hunk discard d161935 -l ^3,^5-7 # discard excluding specific lines
--include-matching and --exclude-matching select changed lines by content
instead of position. They match a literal substring, or a regular expression
with --regex. Both are repeatable, OR'd, and case-sensitive, and both fail if
nothing matches. Use only one of -l, --include-matching, and
--exclude-matching.
-l needs exactly one Hunk. Matching applies to every Hunk the targets select,
so a path selects matching lines across a whole file. --include-matching
leaves out a Hunk with no matching line, and --exclude-matching takes it
whole. A Hunk whose every change is excluded is left out.
Line selection accepts any subset of a pure addition or pure deletion. git-hunk
rejects one side of a one-for-one replacement, because that leaves a lone
deletion or addition. Select both lines, match text they share, or pass
--allow-one-sided when the lone half is what you want.
A grouped replacement has several deleted or added lines. A partial selection
of one must take every line on the side the command consumes, plus some lines
of the other side. stage and commit consume deletions, and unstage and
discard consume additions. Staging every deletion and one addition replaces
the old lines with that addition alone. git-hunk rejects any other partial
selection, because nothing says where an unselected line would land.
--allow-one-sided does not change this.
git-hunk checks -l range endpoints against the Hunk before expanding them, and
keeps each side's no-newline state. Submodule pointer changes and whole-file
Hunks have no line selection. Select them whole.
A binary, mode-only, type, or empty tracked file change is a whole-file Hunk.
Plain output labels empty tracked changes as Empty file (added) or
Empty file (deleted). When one file has a mode change and text edits, the mode
and each text range are separate Hunks. Selecting text does not apply the mode
change, and selecting the mode Hunk does not apply text.
Commit
git-hunk commit d161935 -m "fix: ..." # stage a hunk and commit it in one step
git-hunk commit d161935 -l 3,5-7 -m "..." # stage specific lines and commit
git-hunk commit d161935 --exclude-matching debug -m "..." # commit all but matching lines
commit aborts if anything is already staged, so the commit contains exactly
the selected hunks. It accepts the same -l, --include-matching,
--exclude-matching, --regex, and --allow-one-sided selection options as
stage.
JSON output
git-hunk list --json # inventory: every hunk, no body
git-hunk show <id> --json # the same hunks plus a structured per-line body
Both emit a versioned envelope, so consumers can rely on its shape.
schema_version is currently 2. list --json is an inventory without hunk
bodies, and show --json adds a structured lines array. Here is a
show --json hunk. list --json prints the same object without lines:
{
"schema_version": 2,
"hunks": [
{
"id": "d161935000000000000000000000000000000000000000000000000000000000",
"id_stability": "stable",
"file": { "text": "src/main.py" },
"status": "unstaged",
"change_kind": "M",
"a_mode": "100644",
"b_mode": "100644",
"binary": false,
"header": "@@ -10,3 +10,5 @@",
"context_before": { "text": "def main():" },
"additions": 2,
"deletions": 0,
"lines": [
{ "n": 1, "op": " ", "content": { "text": " x = 1" } },
{ "n": 2, "op": "+", "content": { "text": " y = 2" } }
]
}
]
}
| Field | Type | Description |
|---|---|---|
schema_version |
int | Envelope version; bumped on any incompatible change to the shape below. |
hunks |
array | The hunks (empty array when there are no changes). |
id |
string | Full canonical SHA-256 Hunk ID. An untracked entry's ID covers the file's content; it is empty only for an untracked path git-hunk cannot read, such as an embedded repository. Human output uses a unique prefix of at least seven characters. |
id_stability |
string | stable or conditional. An untracked inventory entry reports stable. |
file |
union | Repository path of the changed file, as a byte-safe {text|bytes} union (see below). |
status |
string | One of staged, unstaged, untracked. |
change_kind |
string | Git status letter: A added, D deleted, M modified, T typechange (R/C reserved and currently rejected). Always present. |
a_mode |
string | null |
b_mode |
string | null |
binary |
bool | Whether the change is binary. Always present. |
header |
string | null |
context_before |
union | null |
additions |
int | Number of added lines. |
deletions |
int | Number of removed lines. |
lines |
array | show --json only. The structured body; [] for a whole-file hunk. See below. |
A lines entry is { "n", "op", "content", "no_newline"? }:
| Field | Type | Description |
|---|---|---|
n |
int | 1-based position within the hunk body, the number -l takes. Counts every body line. |
op |
string | " " context, "+" addition, "-" deletion. |
content |
union | The line text without its leading op character, as a {text|bytes} union. |
no_newline |
bool | Present and true only when the line has no trailing newline; consumes no n. |
Any field carrying arbitrary git/source bytes (file, context_before,
lines[].content) is a byte-safe {text | bytes} union: {"text": "..."} for
valid UTF-8, else {"bytes": "<base64>"}. It is always an object, so consumers
have one code path and strict JSON parsers never see a lone surrogate.
A new field is backward-compatible and leaves schema_version alone. Renaming,
removing, or retyping an existing field bumps it. Before schema_version
existed, list --json returned a bare array.
How it works
- Rejects detected rename, copy, and unmerged states.
- Parses staged and unstaged
git diffoutput into one combined Hunk inventory. - Assigns each Hunk a full canonical SHA-256 ID and a unique human prefix.
- Gives members of a Duplicate Hunk group unique Conditional Hunk IDs.
- For staging, reconstructs a minimal patch and pipes it through
git apply --cached. - For discarding, reconstructs a reverse patch and applies it to the working tree.
Text IDs use the Repository path and patch body, including context and newline
state. They exclude @@ ranges, section headings, and staged state. Whole-file
IDs include the actual binary, mode, or type change. An Unchanged Hunk keeps
its ID while complete Hunks move. A partial operation changes the patch content,
so the resulting Hunks get new IDs.
Contributing
Bug reports, feature requests, and pull requests are welcome on GitHub.
Install just 1.58.0 or newer and
uv. Recipes use Bash.
On Windows, install Git for Windows and put its Bash on PATH.
git clone https://github.com/wkentaro/git-hunk.git
cd git-hunk
just setup # install dependencies
just test # run tests
just lint # run linters
License
MIT (LICENSE)
Metadata
Release files for git-hunk 0.6.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| git_hunk-0.6.0.tar.gz | 40.7 kB | With provenance | Details for git_hunk-0.6.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| git_hunk-0.6.0-py3-none-any.whl | Python 3 | none | any | Details for git_hunk-0.6.0-py3-none-any.whl |
Total release size: 80.0 kB
Release files / git_hunk-0.6.0.tar.gz
| Download URL | git_hunk-0.6.0.tar.gz |
|---|---|
| Size | 40.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0859856355c550109505217c6894cae84b97cd8c90f97fae1c0defca3075ba6f
|
|
BLAKE2b-256 checksum How to use checksums |
a15937d1445fbf8d28957e0c159f856fc9e4b15db6417304416acba0cab8de0f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logRelease files / git_hunk-0.6.0-py3-none-any.whl
| Download URL | git_hunk-0.6.0-py3-none-any.whl |
|---|---|
| Size | 39.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
267f0bbf6685fd93418966e6c9f468a51c6713a5abf124a37f6cbea7c6996fab
|
|
BLAKE2b-256 checksum How to use checksums |
9d828a25cb8f057a7c996983c86cddd33c21daa25a39854a1d4f6759c6365e68
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logSecurity for git-hunk 0.6.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for git_hunk-0.6.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.