Non-interactive, hunk-addressable git staging CLI for LLMs.
git add -p is an interactive TUI that LLMs can't drive. squire
exposes the same hunk-level staging through single commands with
structured arguments, so an LLM (or script) can selectively stage,
unstage, revert, and show hunks without any interactive prompts. It also
provides branch cleanup analysis to identify merged, squash-merged,
and stale branches.
Every hunk gets a short, stable, content-based ID (first 8 hex chars of the hunk content's SHA-256 hash). Each line within a hunk also gets a short content hash (shortest unique prefix, min 2 hex chars). Use these IDs and line hashes to reference hunks and individual lines across commands.
cargo install --path .Requires Rust 1.85+ and git on your PATH.
Default output is human-readable plain text. Global flags:
--json— structured JSON output--short— compact one-line-per-hunk summary (ID, file, range, +/- counts)--llm-help— print a comprehensive reference for LLM consumption, then exit
squire diff # unstaged working tree changes (includes untracked)
squire diff --cached # staged changes
squire diff HEAD~1 # working tree vs ref
squire diff HEAD~1 HEAD~2 # ref vs ref
squire diff -- src/main.rs # filter by path
squire diff --json # output as JSONsquire show abc12345 # hunk from working tree or staged
squire show HEAD abc12345 # hunk from last commit (falls back to diff)
squire show HEAD~2 abc12345 # hunk from two commits agosquire stage abc12345 def67890 # stage specific hunks
squire stage abc12345:f3,a1 # stage specific lines by hash
squire stage abc12345:f3-7b # stage a range of lines
squire unstage abc12345 # unstage specific hunks
squire revert abc12345 # discard changes from working tree
squire revert abc12345:f3,a1 # revert specific linesRevert works on both unstaged and staged hunks. Staged hunks are unstaged and reverse-applied in one step.
squire reword HEAD -m "new message" # reword HEAD
squire reword HEAD~2 -m "fix: corrected typo" # reword older commitFor HEAD, delegates to git commit --amend -m. For older commits,
uses a non-interactive rebase with reword. Requires a clean working
tree for non-HEAD targets.
squire drop HEAD abc12345 # drop hunk from HEAD
squire drop HEAD~2 abc12345 def67890 # drop hunks from older commitInverse of amend: removes specific hunks from an existing commit.
Find hunk IDs with squire diff <commit>~1 <commit> or
squire log --json. Requires a clean working tree for non-HEAD
targets.
squire commit -m "feat: parser" abc12345 # stage + commit in one step
squire amend abc12345 # amend into HEAD
squire amend -m "new msg" abc12345 # amend HEAD with new message
squire amend --commit HEAD~2 abc12345 # amend into an older commitWhen --commit targets a non-HEAD commit, squire creates a fixup
commit and runs an autosquash rebase to fold it in. The -m flag
is only supported when amending HEAD.
squire status # plain text summary
squire status --json # structured outputUntracked files always appear as new-file hunks in squire diff, so you
can stage them with the same workflow as modified files.
# See all changes including new files
$ squire diff
--- b/src/new_module.rs ---
[a1b2c3d4] @@ -0,0 +1,40 @@
+... entire new file as a single hunk ...
# Stage the whole new file by hunk ID
$ squire stage a1b2c3d4
$ git commit -m "feat: add new_module"squire cleanup # auto-detect master branch
squire cleanup --master main # specify master branch
squire cleanup --json # structured output for LLMAnalyzes local branches and classifies each as:
- MERGED — fully merged via git ancestry
- MERGED_EQUIVALENT — commit messages and patches match master (squash/cherry-pick merge)
- NEEDS_EVALUATION — some commit messages match master but patches differ; an LLM should review these commits to determine if the branch is fully merged
- UNMERGED — no matching commits found in master
squire split <commit> # prepare to split a commitRequires a clean working tree. Resets the target commit so its
changes are unstaged, ready for selective re-staging with squire stage.
For HEAD, this is a simple mixed reset. For older commits, squire runs a non-interactive rebase that pauses at the target commit and resets it.
# Split the most recent commit into two
$ squire split abc1234
$ squire diff --json # see the unstaged changes
$ squire stage <id1> <id2> # stage hunks for first commit
$ git commit -m "feat: part one"
$ squire stage <id3> # stage remaining hunks
$ git commit -m "feat: part two"
# Split an older commit (rebase pauses at the commit)
$ squire split def5678
$ squire diff --json
$ squire stage <id> && git commit -m "first half"
$ squire stage <id> && git commit -m "second half"
$ GIT_EDITOR=true git rebase --continue # replay remaining commitsGIT_SEQUENCE_EDITOR="squire seqedit edit:abc1234" git rebase -i HEAD~3
GIT_SEQUENCE_EDITOR="squire seqedit fixup:abc1 drop:def5" git rebase -i HEAD~5squire seqedit rewrites a git rebase todo file, replacing sed/awk
one-liners. It accepts one or more action:sha-prefix arguments
followed by the todo file path (passed automatically by git).
Supported actions: pick, reword, edit, squash, fixup, drop.
squire squash HEAD~2 HEAD~1 HEAD # fold last 2 commits into HEAD~2
squire squash abc1234 def5678 # fold def5678 into abc1234
squire squash -m "combined" abc1234 def5678 # squash with new messageFolds one or more source commits into a target commit. The first
argument is the target (survives), the rest are folded in. The
target's message is kept by default; use -m to replace it.
Requires a clean working tree.
squire stash abc12345 # stash one hunk
squire stash -m "wip" abc12345 # stash with a message
squire stash abc12345:f3,a1 # stash specific lines
squire stash abc12345 def67890 # stash multiple hunksRemoves the selected hunks from the working tree and saves them as a
regular git stash entry. Other unstaged changes are preserved. Use
git stash pop to restore — no special squire command needed.
squire rebase # plain text playbook
squire rebase --onto origin/main # override upstream
squire rebase --json # structured outputPrints a contextualized rebase playbook. Inspects the repo state and emits step-by-step instructions adapted to where you are: pre-rebase, mid-rebase with conflicts, or up-to-date. Creates a safety tag before the first rebase for easy recovery.
When mid-rebase with conflicts, the output includes:
current_commit— the SHA and message of the commit being replayedours_theirs— clarifies that during rebase, "ours" is the upstream and "theirs" is your commit (the opposite of merge)step/total_steps— rebase progress (e.g. step 2 of 3)
Use --onto to override the upstream ref — for example, to rebase
onto a different branch than the configured tracking branch.
squire wraps standard git primitives:
git diffandgit diff --cachedfor working tree and index diffsgit show <sha>for commit diffsgit apply --cachedto stage patchesgit apply --cached --reverseto unstage patchesgit apply --reverseto revert working tree changes
squire parses unified diff output, assigns content-hash IDs to each hunk,
and reconstructs patches from selected hunks when staging or
unstaging.
squire adds value where git's interface is interactive, unstructured, or hunk-unaware. It does not wrap git commands that already work fine non-interactively. For example:
- Stage an entire file →
git add <file>(no squire needed) - Restore a stash →
git stash pop(no squire needed) - Resolve conflicts →
git add+GIT_EDITOR=true git rebase --continue(no squire needed) - Abort a rebase →
git rebase --abort(no squire needed)
squire detects and reports conflicts with structured output so an LLM or script can decide what to do, but it doesn't try to resolve them or wrap the recovery commands.
When a rebase-based command (amend --commit, drop, squash,
reword) hits a conflict, squire returns a structured error instead
of forwarding opaque git stderr:
{
"conflict": true,
"conflicting_files": [
{ "file": "src/lib.rs", "status": "both_modified",
"strategy": "non_trivial", "command": "show the diff and ask for guidance" }
],
"current_commit": { "sha": "abc1234...", "message": "feat: parser" },
"ours_theirs": {
"ours": "upstream (origin/main)",
"theirs": "your commit being replayed"
},
"hint": "Resolve conflicts, stage with `git add`, then run `GIT_EDITOR=true git rebase --continue`. To cancel: `git rebase --abort`."
}Plain text output:
Replaying: abc1234f feat: parser
Conflict during rebase:
both_modified: src/lib.rs → show the diff and ask for guidance
Note: "ours" = upstream (origin/main), "theirs" = your commit
Resolve conflicts, stage with `git add`, then run `GIT_EDITOR=true git rebase --continue`.
To cancel: `git rebase --abort`.
squire status also reports conflicts when a rebase is paused:
{
"branch": "HEAD",
"rebase_in_progress": true,
"conflicts": [
{ "file": "src/lib.rs", "status": "both_modified",
"strategy": "non_trivial", "command": "show the diff and ask for guidance" }
],
"staged": [],
"unstaged": [],
"staged_lines": { "added": 0, "removed": 0 },
"unstaged_lines": { "added": 0, "removed": 0 }
}The conflicts field is only present when there are unresolved
conflicts. Possible status values: both_modified, both_added,
deleted_by_us, deleted_by_them.
Pass --json to get structured output from any command. diff and
show return an array of hunk objects:
[
{
"id": "abc12345",
"file": "src/main.rs",
"old_file": "src/main.rs",
"old_range": "10,5",
"new_range": "10,7",
"content": " ctx\n-old\n+new\n ...",
"header": "fn main()",
"line_hashes": ["e0", "90", "ca", "..."]
}
]The header field is present only when the @@ line includes a
section header (for example, a function name).
stage, unstage, revert, and stash return a result object:
{ "staged": 2, "message": "Staged 2 hunk(s)" }When line selectors are used, a new_hunks array is included with
the residual hunks (id, file, ranges, header, line_hashes). This
avoids needing to re-run squire diff after partial line operations.
The array is omitted when empty.
status returns branch info, rebase state, hunks, and line counts.
The staged and unstaged arrays contain the same hunk objects as
diff. The conflicts array is present only during a paused rebase
with unresolved conflicts:
{
"branch": "main",
"rebase_in_progress": false,
"staged": [{ "id": "...", "file": "...", "..." : "..." }],
"unstaged": [{ "id": "...", "file": "...", "..." : "..." }],
"staged_lines": { "added": 3, "removed": 1 },
"unstaged_lines": { "added": 5, "removed": 2 }
}cargo build
cargo testMIT — see LICENSE.