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, and a contextualized rebase playbook that guides
an LLM through rebases and conflict resolution.
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.
Generate a steering file so the Kiro CLI knows how to use squire:
squire --llm-help > ~/.kiro/steering/squire.mdDefault 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 ago
squire show df489e67 # all hunks from commit df489e67squire 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.
Any unstaged or staged changes are preserved across the operation.
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 — all commit patches are present in master (squash/cherry-pick merge); includes reworded cherry-picks
- NEEDS_EVALUATION — some commits appear merged but others do not;
each commit shows a
best_matchwith message/diff similarity scores for LLM evaluation - UNMERGED — no matching commits found in master; commits with
similar messages in master include
best_matchsimilarity scores
squire log # last 10 commits, plain text
squire log -n 5 # last 5 commits
squire log --short # one line per commit
squire log --json # structured output with hunks
squire log --max-hunk-lines 0 # no per-commit content capLists recent commits with their hunks. Hunk IDs match what
squire diff <sha>~1 <sha> would produce, so you can go straight
from squire log to squire split, squire stage, or squire drop.
To keep squire log --json bounded for LLM context windows, per-commit
hunk content is capped at 100 lines by default. Summary fields
(id, file, old_range, new_range, header) are always kept;
truncated bodies are replaced with a marker that points at
squire show <sha> <id> so you can fetch the full body on demand. The
commit SHA is required in the marker because squire show without a
ref only searches the working tree. Pass --max-hunk-lines 0 to
disable the cap.
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.
Any unstaged or staged changes are preserved across the operation.
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.
All rebase-based commands (amend --commit, drop, reword, squash,
split) are atomic by default: if the rebase would leave a conflict,
squire aborts the rebase, rolls back any intermediate commits it created
(e.g. the fixup! commit amend creates), and resets HEAD to where it
was before the command ran. The full pre-command state — HEAD, index,
working tree, and untracked files — is restored exactly. You get a
structured error naming the conflicting file(s) with rolled_back: true,
but no half-finished rebase to clean up.
Use --pause-on-conflict to opt out of rollback. On conflict, the rebase
is left paused so you can resolve the conflict by hand with git add +
GIT_EDITOR=true git rebase --continue. The structured error reports
rolled_back: false and gives continue/abort hints instead.
Rolled-back error (default):
{
"conflict": true,
"rolled_back": 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": "amend could not complete cleanly and was rolled back; the working tree and history are unchanged. To retry and resolve by hand, re-run with `--pause-on-conflict` to leave the rebase paused on the conflicting commit. Alternative: perform a manual rebase ..."
}Paused error (--pause-on-conflict):
{
"conflict": true,
"rolled_back": false,
"conflicting_files": [ ... ],
"current_commit": { ... },
"ours_theirs": { ... },
"hint": "Resolve conflicts, stage with `git add`, then run `GIT_EDITOR=true git rebase --continue`. To cancel and restore the pre-command state: `git rebase --abort` followed by `git reset --hard <pre-command HEAD>`."
}Plain text output (rolled-back case):
Replaying: abc1234f feat: parser
Conflict during amend (rolled back, history unchanged):
both_modified: src/lib.rs → show the diff and ask for guidance
Note: "ours" = upstream (origin/main), "theirs" = your commit
amend could not complete cleanly and was rolled back ...
squire status reports conflicts when a rebase is paused (either from
--pause-on-conflict or from an external git rebase):
{
"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.
