Non-interactive, programmatic alternative to git add -p.
Every hunk gets a stable, content-based ID so you can inspect, filter, and stage changes without interactive prompts.
git add -p requires interactive input. That makes it unusable for:
- AI agents (Claude Code, Codex, etc.) that need to split changes into logical commits
- Scripts & CI/CD that automate commit organization
- Editor integrations that want hunk-level staging without shelling out to a TUI
git-hunk solves this by assigning each hunk a stable ID and exposing simple
stage/unstage/discard commands.
pip install git-hunkOr with uv:
uv tool install git-hunkVerify it works:
git-hunk --versionA usage guide ships inside the CLI, so agents (Claude Code, Codex, etc.) can load it on demand. It always matches the installed version, so it never goes stale:
git-hunk skills get coregit-hunk --help points here first.
# 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"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 scriptinggit-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 hunksgit-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 HEAD
git-hunk discard d161935 -l ^3,^5-7 # discard excluding specific lines--include-matching / --exclude-matching select changed lines by content
instead of line number (literal substring by default, --regex for regular
expressions). Both are repeatable and OR'd, case-sensitive, and error if nothing
matches. They are mutually exclusive with -l and with each other.
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 commitcommit aborts if anything is already staged, so the commit contains exactly
the selected hunks.
git-hunk list --json # inventory: every hunk, no body
git-hunk show <id> --json # the same hunks plus a structured per-line bodyBoth emit a versioned envelope (schema_version is currently 2) so consumers
can depend on a stable shape. list --json is a lean inventory and carries no
body; show --json adds a structured lines array. A show --json hunk
(list --json is identical but without the lines field):
{
"schema_version": 2,
"hunks": [
{
"id": "d161935",
"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 | Stable, content-based hunk id (7-char SHA-256 prefix); accepts prefixes. |
file |
union | 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). Always present. |
a_mode |
string | null | 6-digit octal git mode on the pre-image side; null when that side does not exist. |
b_mode |
string | null | 6-digit octal git mode on the post-image side; null when that side does not exist. |
binary |
bool | Whether the change is binary. Always present. |
header |
string | null | The hunk's bare @@ -a,b +c,d @@ range; null for a whole-file (binary, mode-only, or type) change. |
context_before |
union | null | The function/section git names after the @@ header, as a {text|bytes} union; null when there is none. |
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 index -l line selection uses. 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.
Adding a new field is backward-compatible and does not change schema_version;
renaming, removing, or changing the type of an existing field bumps it. (Before
schema_version existed, list --json returned a bare array.)
| 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 |
- Parses
git diffoutput into individual hunks - Assigns each hunk a stable, content-based ID (SHA-256 prefix)
- 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
IDs are stable across partial staging -- they are derived from the changed lines,
not the @@ line numbers that shift as you stage hunks.
Bug reports, feature requests, and pull requests are welcome on GitHub.
git clone https://github.com/wkentaro/git-hunk.git
cd git-hunk
make setup # install dependencies
make test # run tests
make lint # run lintersMIT (LICENSE)
