Skip to main content

git-hunk

PyPI Python License Build

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.

git-hunk teaser

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

  1. Rejects detected rename, copy, and unmerged states.
  2. Parses staged and unstaged git diff output into one combined Hunk inventory.
  3. Assigns each Hunk a full canonical SHA-256 ID and a unique human prefix.
  4. Gives members of a Duplicate Hunk group unique Conditional Hunk IDs.
  5. For staging, reconstructs a minimal patch and pipes it through git apply --cached.
  6. 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)

Source distribution for git-hunk 0.6.0
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)

Table of built distributions (wheels) for git-hunk 0.6.0
File Interpreter ABI Platform Actions
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page