Commit only a slice of your changes while preserving unrelated work, without going full git wizard.
- Commit selected files and/or line ranges
- Preserve unrelated staged, unstaged, and untracked work
- Protect against concurrent commit and staging conflicts
- Use it in scripts, automation, and concurrent agent workflows
- Why does this exist?
- Is it safe?
- When to use
- Installation
- Usage
- Behavior
- Limits
- Recovery
- Design and safety
- Development
- License
To solve the pain of committing a subset of changes that arise from working with multiple concurrent agents and user edits. Without it, you either:
- Use Git worktrees, which add setup and teardown and may require merging
- Execute a fragile sequence of Git operations:
- save the existing staged state
- stash unrelated unstaged and untracked work
- reset and rebuild staging with only the desired files or hunks
- create the commit
- restore the stash and previous staged state
The solution is a CLI tool that replicates what most IDE commit tool windows do:
- Select one or more changed files
- Select changed regions within a file
- Commit those selections with a message
This CLI is nearly 100% vibed, so make of that what you will. But at least for me, I'm confident in it because of:
- My extensive daily use of this tool across many projects
- A comprehensive test suite covering behavior and safety
Hopefully that gives you confidence too. For more info, see the safety model.
Commit all current changes?
├─ ✓ yes ─► git commit
└─ ✗ no
│
▼
Only complete changes to tracked files?
├─ ✓ yes, ordinary workflow ─► git commit -m "..." -- <paths>
└─ ✗ no, or stronger isolation needed
│
▼
Choose hunks interactively?
├─ ✓ yes ─► git add -p, an IDE, or a Git TUI
└─ ✗ no
│
▼
Can you name the exact files or current line ranges?
├─ ✗ no ──► use another Git workflow
└─ ✓ yes
│
▼
◆ subcommit
├─ commits only the requested selection
├─ accepts untracked files without prior staging
├─ preserves unrelated staged, unstaged, and untracked work
├─ guards against concurrent commit and staging conflicts
└─ prevents hooks from widening the selection
Note
subcommit requires Git 2.36 or later. It has no other runtime dependencies.
Install with Mise:
mise use -g github:reemus-dev/subcommitmacOS and Linux (-d <directory> to install elsewhere):
# Install in `$HOME/.local/bin`
curl -fsSL https://raw.githubusercontent.com/reemus-dev/subcommit/main/assets/install.sh | bash
# Install system-wide
curl -fsSL https://raw.githubusercontent.com/reemus-dev/subcommit/main/assets/install.sh | sudo bash -s -- -d /usr/local/binWindows users:
# Installs to `%LOCALAPPDATA%\Programs\subcommit\bin` and adds the directory to `PATH`
irm https://raw.githubusercontent.com/reemus-dev/subcommit/main/assets/install.ps1 | iexBoth scripts verify the release checksum and install subcommit and
git-subcommit.
Download an archive from the
GitHub Releases page, then
copy subcommit and/or git-subcommit to a directory on PATH.
git clone https://github.com/reemus-dev/subcommit.git
cd subcommit
mise install
mise run buildThe binaries are written to bin/. Copy subcommit and git-subcommit, with
.exe suffixes on Windows, to a directory on PATH.
There are two optional agent skills. Both require subcommit to be installed.
| Skill | Summary |
|---|---|
subcommit |
Minimal skill to make subcommit known and delegates usage guidance to the CLI |
commit |
Opinionated workflow for scope, grouping, and messaging |
Install either skill using the skills CLI:
# Choose interactively
npx skills add reemus-dev/subcommit
# Select a specific skill
npx skills add reemus-dev/subcommit --skill subcommit
npx skills add reemus-dev/subcommit --skill commitGenerate completion for Bash, Zsh, Fish, or PowerShell:
subcommit --completion bash
subcommit --completion zsh
subcommit --completion fish
subcommit --completion powershellSource it appropriately in your shell configuration file, example:
# ~/.bashrc
source <(subcommit --completion bash)
# ~/.zshrc
source <(subcommit --completion zsh)Release archives include generated completion files under completions/. Bash
and Zsh completions also support git subcommit. File targets use shell path
completion. Append line ranges manually.
# Direct
subcommit [<path|path:ranges>...] (-m <message> | -F <file>) [flags]
# Git integration
git subcommit [<path|path:ranges>...] (-m <message> | -F <file>) [flags]Examples:
subcommit -m "..." path/file.ext
subcommit -m "..." path/file.ext:42-48
subcommit -m "..." path/file.ext:5 path/file.ext:42-48
subcommit -m "..." path/file.ext:5,42-48Each target selects either an entire file or changes touching specified lines.
Entire file: pass a path without a range suffix. This supports tracked changes and deletions, untracked files, binary files, and symbolic links.
Lines: append line numbers or ranges to the path:
:12selects changes touching line 12:12-20selects changes touching lines 12 through 20:12,30-40combines multiple ranges
Line numbers refer to the file as it currently appears on disk. For a deletion with no replacement line, select an adjacent current line. If no current line remains, select the entire file.
Multiple ranges may be comma-separated or passed by repeating the same path.
subcommit treats targets as literal paths rather than as Git patterns. Your
shell may still interpret characters such as *, ?, [], or parentheses, so
quote paths containing them. Use --complete when a literal filename ends in
valid range syntax, such as report:42.
subcommit --complete report:42 -m "..."Every commit requires exactly one message source.
Use -m to supply the message directly:
subcommit path/file.ext -m "..."Use -F to read a longer message from a file:
subcommit path/file.ext -F message.txt-F - reads the message from standard input. Pass --yes when piping input
because interactive confirmation is unavailable:
printf 'subject\n\nbody\n' | subcommit path/file.ext -F - --yesBy default, subcommit previews the candidate commit and asks for confirmation.
The preview shows a diff summary, the effective patch for line-selected files,
the commit message, accepted hook changes, and unrelated work that will remain
uncommitted.
Use --yes to skip confirmation. It is required when no interactive terminal is
available.
--quiet suppresses the preview and most informational output. It does not skip
confirmation and still reports hook changes, errors, and recovery guidance.
On success:
stdout:committed: <full-sha>stderr: previews, prompts, hook output, warnings, and errors
| Flag | Meaning |
|---|---|
-m, --message |
Use the supplied commit message |
-F, --file |
Read the message from a file, or stdin with - |
--complete <path> |
Select a complete literal path, repeatable |
-y, --yes |
Publish without interactive confirmation |
-n, --no-verify |
Skip pre-commit and commit-msg hooks |
-q, --quiet |
Suppress preview and informational output |
-v, --verbose |
Show every preserved path |
--color |
Set color to auto, always, or never |
--completion |
Generate completion for a supported shell and exit |
subcommit favors predictable scope over best-effort commits. If it cannot
commit every requested target without affecting unrelated work, it refuses the
whole operation.
Selection comes from the difference between the last commit and the files currently on disk. Existing staging does not determine what gets committed.
After a successful commit:
- Selected changes are committed
- Unrelated staged, unstaged, and untracked work remains in place
- Prior staging on a selected path is replaced by the successful selection
- Unselected changes in a partially committed file remain available
Every requested target must contribute a change. If a complete target is
unchanged on disk but has staged-only changes, subcommit refuses rather than
discarding them.
Important
A line number identifies a change, not an exact commit boundary. If that line belongs to a larger contiguous edit, the entire edit is committed. Review the effective patch before confirmation.
Repeated ranges for one file are combined. Selecting both the entire file and specific lines from it is refused rather than silently broadening the commit. Line selection supports modified regular files only.
If one file disappears and an identical new file appears, subcommit treats
them as a move. Selecting either path automatically includes both. Example:
git mv a.txt b.txt
subcommit -m "rename a to b" b.txtIf the moved file was also edited, or more than one identical match exists, name both paths explicitly:
subcommit -m "move and edit" a.txt b.txt- Directories: cannot be selected directly.
- Symbolic links: are supported as complete targets, not line selections.
- Symlinked directories: tracked files reached through them follow
git commitbehavior. Untracked files reached through them are refused likegit add. - Sparse checkout: a file omitted from disk is treated as unchanged, not deleted. An existing selected file remains authoritative.
- Executable permissions: follow the repository's
core.fileModesetting. - Commit signing: follows
commit.gpgsign. Malformed values are refused like Git.
subcommit itself does not rewrite files.
Warning
Hooks run before confirmation and may modify selected files. File changes are reported, but canceling does not undo them.
| Hook | Behavior |
|---|---|
pre-commit |
Runs against selected changes unless --no-verify is set |
prepare-commit-msg |
Always runs and may rewrite the message |
commit-msg |
Runs unless --no-verify is set and may rewrite or reject the message |
post-commit |
Runs after the commit is published |
After pre-commit, subcommit verifies that:
- complete-file targets may be formatted and restaged, but remain changed
- line-selected targets were not modified
- inferred moves still contain both endpoints
- unrelated changes were not added
Any scope change causes refusal. A failing post-commit produces a warning
because the commit has already succeeded.
| Situation | Behavior |
|---|---|
| No existing commit | Refuses. Create the initial commit with Git first. |
| Merge, rebase, cherry-pick, revert, or bisect in progress | Refuses until the operation is completed or aborted. |
Current commit changes while subcommit runs |
Refuses and does not overwrite the newer commit. |
Files change while subcommit runs |
Files are not locked. The captured version may be committed while later edits remain on disk. |
| Another process ignores Git's index lock | Its changes are outside subcommit's concurrency guarantees. |
Publishing requires updating both the current commit and Git's staging area. Those updates cannot be one filesystem transaction.
| Failure point | Repository state | Recovery |
|---|---|---|
| Before the current commit changes | The original commit and staging remain current. The candidate commit still exists. | Use the printed git cherry-pick <sha> command or retry. |
| After the current commit changes | The new commit is current, but the staging update may be incomplete. Recovery files are retained. | Inspect the retained files and follow the printed recovery instructions. |
flowchart TD
select["Compare requested paths<br/>last commit → files on disk"]
select --> candidate["Build proposed commit<br/>in temporary staging"]
candidate --> hooks["Run hooks and verify scope"]
hooks -->|Invalid scope| refuse["Refuse without publishing"]
hooks -->|Valid| preview["Show final preview"]
preview --> confirm{"Confirmed?"}
confirm -->|No| refuse
confirm -->|Yes or --yes| publish["Publish commit"]
publish --> preserve["Update selected staging<br/>preserve unrelated work"]
subcommit refuses rather than guesses when it cannot preserve the requested
scope safely.
✅ Covered and tested · ❌ Not guaranteed or intentionally refused
| Status | Area | Behavior |
|---|---|---|
| ✅ | Requested scope | Every target must contribute a change, or no commit is published. |
| ✅ | Unrelated work | Successful commits preserve unrelated staged, unstaged, and untracked work. |
| ✅ | Existing staging | Previously staged changes cannot leak into the commit. |
| ✅ | Partial files | Changes outside selected lines remain available. |
| ✅ | Hook scope | Hooks cannot silently broaden path scope or mutate line-selected files. |
| ✅ | Literal paths | Filenames are not interpreted as Git patterns. |
| ✅ | Concurrent commits | A newer commit is not overwritten. A losing operation remains retryable. |
| ✅ | Concurrent staging | Normal Git writers are coordinated through Git's index lock. |
| ✅ | Recovery | Failures report the candidate commit or retained recovery files. |
| ❌ | Worktree locking | Files are not frozen while the command runs. |
| ❌ | Automatic coordination | Concurrent operations are not queued or merged automatically. |
| ❌ | Hook rollback | Canceling does not undo file changes made by hooks. |
| ❌ | Non-cooperative writers | Processes that ignore Git's locking protocol are not protected against. |
| ❌ | Atomic publication | The commit and staging update cannot be one filesystem transaction. |
| ❌ | Complex Git operations | Active merges, rebases, cherry-picks, reverts, and bisects are refused. |
Install the pinned toolchain and run the canonical checks:
mise install
mise run checkOther canonical tasks include mise run build, mise run test,
mise run lint, and mise run format. Regenerate the README terminal demo with
mise run demo on macOS or Linux. The task runs
VHS in Docker and is intentionally
excluded from CI.
Before tagging a release, run mise run release:check and inspect the artifacts
from mise run release:snapshot. Pushing a v* tag runs the release workflow
and publishes the GitHub Release with the mise-managed GoReleaser.
