git-surgeon
git-surgeon gives AI agents surgical control over git changes without
interactive prompts. Stage, unstage, or discard individual hunks. Commit hunks
directly with line-range precision. Restructure history by splitting commits or
folding fixes into earlier ones.
Highlights
- Stage specific lines from a hunk, not just whole hunks (example)
- Split commits that mix concerns into focused commits (example)
Quick start
1. Install
# Shell
|
# Cargo
# Homebrew
2. Install the AI assistant skill
# Claude Code
# OpenCode
# Codex
Alternatively, for Claude Code via the plugin marketplace:
3. Use it
Ask Claude Code to make granular commits. It will use git-surgeon automatically to stage individual hunks instead of entire files.
Commands
hunks— List hunks in the diffshow— Show full diff for a specific hunkstage— Stage hunks by IDcommit— Stage hunks and commit in one stepunstage— Unstage hunks by IDdiscard— Discard working tree changes for hunksfixup— Fold staged changes into an earlier commitreword— Change the commit message of an existing commitsquash— Squash multiple commits into oneundo— Reverse-apply hunks from a commitsplit— Split a commit into multiple commits by hunk selection
hunks
Lists all hunks with their IDs, file paths, function context, change counts, and a preview of changed lines.
# List unstaged hunks
# List staged hunks
# Filter to a specific file
# List hunks from a specific commit
# Show full diff with line numbers (useful for small commits)
Example output
a1b2c3d src/main.rs fn handle_request (+3 -1)
- let result = process(input);
+ let result = match process(input) {
+ Ok(v) => v,
+ Err(e) => return Err(e),
+ };
e4f5678 src/lib.rs (+1 -0)
+use std::collections::HashMap;
Each line shows: <hunk-id> <file> [function context] (+additions -deletions)
show
Shows the full diff (header + all lines) for a single hunk. Each line is
prefixed with a 1-based line number for use with --lines.
# Show a hunk from a specific commit
Example output
@@ -1,4 +1,6 @@ fn main
1: context
2:-deleted line
3:+added line
4: context
Searches both unstaged and staged diffs when no --commit is specified.
stage
Stages one or more hunks by ID. Equivalent to selectively answering "y" in
git add -p.
# Stage only lines 5-30 of a hunk
commit
Stages hunks and commits them in a single step. Equivalent to running stage
followed by git commit. If the commit fails, the hunks are unstaged to restore
the original state. Refuses to run if the index already contains staged changes.
# With inline line ranges
unstage
Unstages one or more previously staged hunks, moving them back to the working tree.
# Unstage only lines 5-30 of a hunk
discard
Discards working tree changes for specific hunks. This reverse-applies the
hunks, effectively running git checkout -p non-interactively.
# Discard only lines 5-30 of a hunk
Warning: This permanently removes uncommitted changes for the specified hunks.
fixup
Folds currently staged changes into an earlier commit. Uses git commit --amend
for HEAD, or an autosquash rebase for older commits. Unstaged changes are
preserved via --autostash.
# Stage some hunks, then fixup an earlier commit
# Fixup HEAD (equivalent to git commit --amend --no-edit)
If the rebase hits a conflict, the repo is left in the conflict state for manual
resolution (git rebase --continue or git rebase --abort).
reword
Changes the commit message of an existing commit without modifying its content.
Uses git commit --amend for HEAD, or an autosquash rebase for older commits.
# With subject + body
# Change an earlier commit's message
If the rebase hits a conflict, the repo is left in the conflict state for manual
resolution (git rebase --continue or git rebase --abort).
squash
Combines commits from <commit> through HEAD into a single commit.
# Squash last 2 commits
# Squash last 3 commits with subject + body
# Squash from a specific commit
# Force squash even if range contains merge commits
The target commit must be an ancestor of HEAD. If the range contains merge
commits, use --force to flatten them into the squashed commit. Uncommitted
changes are autostashed and restored after squashing. The author from the oldest
commit is preserved by default; use --no-preserve-author to use the current
user instead.
undo
Reverse-applies hunks from a specific commit onto the working tree. Useful for selectively reverting parts of a previous commit without reverting the entire commit.
# List hunks from the commit to find IDs
# Undo specific hunks
# Undo only lines 2-10 of a hunk
The changes appear as unstaged modifications in the working tree. Fails gracefully if context lines have changed since the commit (the patch no longer applies cleanly).
split
Splits an existing commit into multiple commits by selecting which hunks go into each new commit. Works on HEAD (direct reset) or earlier commits (via rebase).
# Split HEAD into two commits
# With subject + body (multiple -m flags, like git commit)
# With line ranges (id:range inline syntax)
# Use commas for non-contiguous ranges
# Multiple splits (more than two commits)
Each --pick group specifies hunk IDs (with optional :start-end line ranges)
followed by -m/--message. Use multiple -m flags to add a commit body
(joined with blank lines, like git commit). Remaining unpicked hunks are
committed with --rest-message (defaults to the original commit message if
omitted; supports multiple values for body).
Requires a clean working tree. For non-HEAD commits, uses interactive rebase
with --autostash.
How hunk IDs work
IDs are 7-character hex strings derived from SHA-1 of the file path and hunk
content (the actual +/-/context lines, excluding the @@ header). This
means:
- IDs are stable across line shifts — adding lines above a hunk doesn't change its ID
- IDs are deterministic — the same content always produces the same ID
- Collisions get a
-2,-3suffix (e.g.,a1b2c3d-2)
Typical AI agent workflow
# 1. Agent makes changes to multiple files
# 2. Review what changed
# 3. Stage and commit hunks for feature A in one step
# 4. Commit remaining hunks for feature B
Example: splitting a hunk across commits
A file has two unrelated changes (pagination + soft-delete filtering) that git
groups into a single hunk. The agent uses show to see line numbers, then
--lines to stage only the pagination part:
❯ Commit the changes separately
⏺ Bash(git diff)
⎿ diff --git i/users.py w/users.py
index c3c39df..24929a5 100644
--- i/users.py
+++ w/users.py
@@ -1,13 +1,17 @@
import json
-def list_users(db):
+def list_users(db, page=1, per_page=50):
"""Return all active users."""
- users = db.query("SELECT * FROM users WHERE active = 1")
- return users
+ offset = (page - 1) * per_page
+ users = db.query("SELECT * FROM users WHERE active = 1 LIMIT ? OFFSET ?", per_page, offset)
+ total = db.query("SELECT COUNT(*) FROM users WHERE active = 1")[0]
+ return {"users": users, "total": total, "page": page}
def get_user(db, user_id):
"""Fetch a single user by ID."""
- user = db.query("SELECT * FROM users WHERE id = ?", user_id)
+ user = db.query("SELECT * FROM users WHERE id = ? AND deleted_at IS NULL", user_id)
+ if not user:
+ return None
return user
⏺ There are two logical changes here: pagination for list_users and
soft-delete filtering + null handling for get_user. Let me commit them
separately using git-surgeon.
⏺ Skill(git-surgeon)
⎿ Successfully loaded skill
⏺ Bash(git-surgeon hunks)
⎿ 64ba255 users.py (+8 -4)
-def list_users(db):
+def list_users(db, page=1, per_page=50):
- users = db.query("SELECT * FROM users WHERE active = 1")
... (+8 more lines)
⏺ Only one hunk. Let me see the full diff with line numbers to split it.
⏺ Bash(git-surgeon show 64ba255)
⎿ @@ -1,13 +1,17 @@
1: import json
2:
3:-def list_users(db):
4:+def list_users(db, page=1, per_page=50):
... +15 lines (ctrl+o to expand)
⏺ Bash(git-surgeon stage 64ba255 --lines 1-11)
⎿ 64ba255
⏺ Bash(git commit -m "add pagination to list_users")
⎿ [feature-users e81ede5] add pagination to list_users
1 file changed, 5 insertions(+), 3 deletions(-)
⏺ Bash(git add users.py && git commit -m "filter soft-deleted users in get_user")
⎿ [feature-users de99d59] filter soft-deleted users in get_user
1 file changed, 3 insertions(+), 1 deletion(-)
Example: splitting a commit that mixes concerns
A commit mixes three unrelated changes: logging, pagination, and active user
filtering. The agent uses hunks --commit to see the changes, show --commit
to examine line numbers, then split with line ranges to separate concerns:
❯ split the big pagination commit that mixes concerns into multiple commits
⏺ Skill(git-surgeon)
⎿ Successfully loaded skill
⏺ I'll help you split the pagination commit. Let me first look at the commit
history to identify it, then examine its hunks.
⏺ Bash(git log --oneline -5)
⎿ dda5d0e Add readme
945381f Add pagination, logging, and active user filtering
386b2fe Initial app setup
⏺ Bash(git-surgeon hunks --commit 945381f --full)
⎿ a811273 config.py (+3 -1)
1: DATABASE_URL = "sqlite:///app.db"
2: SECRET_KEY = "dev-secret-key"
… +76 lines (ctrl+o to expand)
⏺ I can see the commit mixes several concerns:
1. Logging - LOG_LEVEL, LOG_FORMAT, logging imports, logger.info/warning calls
2. Pagination - MAX_PAGE_SIZE, pagination logic in list_users
3. Active user filtering - active field in User model and data, filtering in list_users
Let me split this into three focused commits:
⏺ Bash(git-surgeon split 945381f \
--pick a811273:4-6 8c1940f:2,5-6,34,37 \
-m "Add logging infrastructure" \
-m "Configure debug logging level and format, add request l…)
⎿ committed: Add logging infrastructure
committed: Add active flag to users
committed: Add pagination support
⏺ Done. Let me verify the split:
⏺ Bash(git log --oneline -5)
⎿ f5d7d3e Add readme
ff73e29 Add pagination support
d6d0a7b Add active flag to users
… +2 lines (ctrl+o to expand)
⏺ The commit has been split into three focused commits:
┌─────────┬─────────────────────────────────────────────────────────────────────────────────────────────┐
│ Commit │ Description │
├─────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
│ 6f98f7e │ Add logging infrastructure - LOG_LEVEL, LOG_FORMAT config, logging imports and logger calls │
├─────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
│ d6d0a7b │ Add active flag to users - active field in model/data, filtering logic │
├─────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
│ ff73e29 │ Add pagination support - MAX_PAGE_SIZE config, page/per_page params, paginated response │
└─────────┴─────────────────────────────────────────────────────────────────────────────────────────────┘
✻ Sautéed for 35s
Requirements
- Git 2.0+
- Rust (for building from source)