Repository files navigation

preen

preen

Latest releaseLicense

Clean up your commit history, automatically.

preen demo

You got in the zone and came out with forty changed files and no commits, or one giant blob with a message like wip that you are not proud of. preen turns that into a clean, ordered set of atomic commits: the shape of the history you would have written if you had committed carefully as you went.

Think git add -p and git rebase -i, done for you. preen reads everything you changed, groups it into coherent commits, writes a subject for each, orders them so the history bisects, and shows you the plan first. Nothing moves until you approve.

The built-in subjects say where, not why: Add api, Update dependencies. For messages that explain intent, hand grouping to any program you like with --grouper, a model included, and reword anything at the approval prompt.

Clean history is worth having on its own. It makes review readable, git bisect useful, and git blame honest. preen gets you there without the tedious hand-staging.

It cannot lose your work

preen only reshapes history, never content, and it enforces that rather than promising it.

Before a run it hashes a tree holding your HEAD plus every staged, unstaged, and untracked change. After the run it hashes the same thing again. The two must match exactly. If a single byte differs, the run rolls itself back to the recovery branch it made before it started and tells you which paths diverged.

Every run leaves a preen-backup/<timestamp> branch, and preen restore puts you back where you were, with your work returned to the working tree exactly as it was.

Install

go install github.com/dcadolph/preen@latest

preen is a single binary. It needs git on your PATH and nothing else: no model, no API key, no network.

Use

Run it against a dirty working tree:

preen

You get a plan like this:

Planned commits (4):
1. Update dependencies
go.mod
2. Add api
api/server.go
api/server_test.go
3. Add store
store/db.go
4. Add guide.md
docs/guide.md
Apply this plan? 4 commits [y/n, or ? for edits]:

Approve and it stages each group precisely, commits, verifies your content is unchanged, and tells you how to undo it. Or edit the plan first, one move at a time, with the full plan reshown after each:

merge 2 into 1 fold one commit into another
split 3 break a commit into one per file
move api/x.go to 2 reassign a file
reword 1 Add parser replace a subject
drop scratch.txt leave a file uncommitted
reorder 3,1,2 resequence

An edit that would stop the plan covering your tree is rejected, so the prompt cannot walk you into losing a change.

What it does

  • Surveys every uncommitted change: staged, unstaged, and untracked.
  • Groups them into atomic commits, one coherent idea each, ordered so dependencies land first and the history could be bisected.
  • Absorbs a run of unpushed commits back into the tree and redoes them clean with --absorb, no manual reset.
  • Folds dirty changes into the unpushed commits that introduced them with --fixup, then squashes them away with an autosquash rebase.
  • Refuses to redo a commit a remote already has, and moves its base forward past any merge whose side branch is published.
  • Rewrites published history only when you ask twice, with --pushed and, on a shared branch, --allow-protected, then pushes with --force-with-lease behind a separate confirmation.
  • Runs your build or test gate after each commit with --gate, rolling the whole run back on failure.
  • Preens only part of the tree with --scope, leaving the rest dirty.
  • Plans without acting with --dry-run, and skips the approval prompt with --yes for scripted runs.
  • Reports debug prints, scratch markers, commented-out code, and skipped tests with --sweep, and never removes any of them.
  • Undoes any run with preen restore, and cleans up old recovery refs with preen backups --prune.

It never invents changes and never touches a commit you did not ask it to.

How grouping works

The grouping is deterministic and needs no model. preen separates dependency manifests, CI configuration, documentation, and configuration from source, then groups source by package, keeps a test file with the code it exercises, keeps rename pairs together, and treats anything you staged by hand as a boundary you drew deliberately. Dependencies are recorded first and documentation last.

Because a fixed rule cannot know whether two hunks in one file are one idea or two, the built-in grouper never splits a file.

When you want that judgment, hand grouping to a program:

preen --grouper ./my-grouper

The program reads a JSON request on stdin holding every changed file and its hunks, and writes back the commits it proposes. It can split one file's hunks across separate commits. The contract is provider agnostic, so any model CLI, script, or service wrapper can be a grouper, and none of them can touch your repository: a grouper only answers, and preen verifies every path and hunk index against the real tree before acting. If it fails, returns nothing, or names something that is not there, the run falls back to the built-in rules rather than trusting it.

Every guardrail is the same either way. The grouper chooses what goes where and nothing else.

Message style

preen writes a short imperative subject by default. Dictate the format with flags:

preen --conventional --prefix ABC-123 --max-subject 50 --no-emdash --no-semicolon

--punctuation auto reads your repository's own recent subjects and follows whatever they do. --body, --include-files, and --include-line-numbers control the message body, with line ranges read from the real hunk headers.

Or set defaults once in a .preen.toml at the repository root:

[commit]
no-emdash = trueno-semicolon = truemax-subject = 50punctuation = "never"conventional = trueprefix = "ABC-123"body = "auto"include-files = false
[run]
gate = "go test ./..."sweep = trueallow-no-verify = false
[protect]
branches = ["develop", "release/*"]

Flags beat the config file, which beats the defaults. Every generated message is checked against the style before it is recorded, so a configured convention is enforced rather than merely requested.

If your repository has hooks that block automated commits, set allow-no-verify = true under [run] to grant standing consent ahead of time. preen never bypasses a hook on its own judgment. A hook that reformats what the run commits would normally trip the conservation check; --allow-hook-rewrites accepts content differences confined to the paths the run committed.

Rewriting published history

preen will redo commits a remote already has, but only when you say so twice. --pushed grants the ask, and on a branch that is shared by name you also need --allow-protected. main, master, trunk, develop, release, and production are protected out of the box, plus anything listed under [protect] in the config, which comes from the repository and can never be dropped by a flag.

preen --pushed --pushed-base origin/main~4

The push is a third, separate confirmation, it shows you the exact command first, and it always uses --force-with-lease so it aborts rather than clobbering work that arrived after your last fetch. Consent is per invocation: the config file cannot grant it.

Commands

preen Group the working tree into commits.
preen restore [ref] Undo a run. Defaults to the most recent backup.
preen backups List recovery refs. --prune deletes the safe ones.

Exit codes are distinct, so a script can tell a rolled-back run (6, 7) from a rejected plan (5) or a declined one (8).

Undo

preen restore

This moves the branch back and returns your work to the working tree exactly as it was: same files, same content, uncommitted. It only ever accepts a preen-backup/ ref, so it cannot move your branch somewhere unrelated.

Development

go test ./...

The tests run against real git repositories in temp directories rather than a mock, because matching git's own index and patch behavior is the whole job. The conservation invariant, the published-merge guard, and the restore round trip each have their own regression test.

Claude Code plugin

The repository is also a Claude Code plugin. Add it as a marketplace and asking Claude to "clean up my commit history" loads a skill that drives the same binary, with every guardrail intact:

/plugin marketplace add dcadolph/preen
/plugin install preen@preen

More tools

  • kibble, test your README's install steps in a clean container
  • slop-chop, strip the AI tells out of your writing
  • vamoose, route time off through approval, then tell the team
  • whodar, find who to talk to about X across your work tools

License

MIT.


preen: what a bird does to put every feather back in place.

About

Split a messy working tree into clean, atomic git commits with real messages, and see the plan before anything moves. Claude Code skill and CLI.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

preen

preen

Latest releaseLicense

Clean up your commit history, automatically.

preen demo

You got in the zone and came out with forty changed files and no commits, or one giant blob with a message like wip that you are not proud of. preen turns that into a clean, ordered set of atomic commits: the shape of the history you would have written if you had committed carefully as you went.

Think git add -p and git rebase -i, done for you. preen reads everything you changed, groups it into coherent commits, writes a subject for each, orders them so the history bisects, and shows you the plan first. Nothing moves until you approve.

The built-in subjects say where, not why: Add api, Update dependencies. For messages that explain intent, hand grouping to any program you like with --grouper, a model included, and reword anything at the approval prompt.

Clean history is worth having on its own. It makes review readable, git bisect useful, and git blame honest. preen gets you there without the tedious hand-staging.

It cannot lose your work

preen only reshapes history, never content, and it enforces that rather than promising it.

Before a run it hashes a tree holding your HEAD plus every staged, unstaged, and untracked change. After the run it hashes the same thing again. The two must match exactly. If a single byte differs, the run rolls itself back to the recovery branch it made before it started and tells you which paths diverged.

Every run leaves a preen-backup/<timestamp> branch, and preen restore puts you back where you were, with your work returned to the working tree exactly as it was.

Install

go install github.com/dcadolph/preen@latest

preen is a single binary. It needs git on your PATH and nothing else: no model, no API key, no network.

Use

Run it against a dirty working tree:

preen

You get a plan like this:

Planned commits (4):
1. Update dependencies
go.mod
2. Add api
api/server.go
api/server_test.go
3. Add store
store/db.go
4. Add guide.md
docs/guide.md
Apply this plan? 4 commits [y/n, or ? for edits]:

Approve and it stages each group precisely, commits, verifies your content is unchanged, and tells you how to undo it. Or edit the plan first, one move at a time, with the full plan reshown after each:

merge 2 into 1 fold one commit into another
split 3 break a commit into one per file
move api/x.go to 2 reassign a file
reword 1 Add parser replace a subject
drop scratch.txt leave a file uncommitted
reorder 3,1,2 resequence

An edit that would stop the plan covering your tree is rejected, so the prompt cannot walk you into losing a change.

What it does

  • Surveys every uncommitted change: staged, unstaged, and untracked.
  • Groups them into atomic commits, one coherent idea each, ordered so dependencies land first and the history could be bisected.
  • Absorbs a run of unpushed commits back into the tree and redoes them clean with --absorb, no manual reset.
  • Folds dirty changes into the unpushed commits that introduced them with --fixup, then squashes them away with an autosquash rebase.
  • Refuses to redo a commit a remote already has, and moves its base forward past any merge whose side branch is published.
  • Rewrites published history only when you ask twice, with --pushed and, on a shared branch, --allow-protected, then pushes with --force-with-lease behind a separate confirmation.
  • Runs your build or test gate after each commit with --gate, rolling the whole run back on failure.
  • Preens only part of the tree with --scope, leaving the rest dirty.
  • Plans without acting with --dry-run, and skips the approval prompt with --yes for scripted runs.
  • Reports debug prints, scratch markers, commented-out code, and skipped tests with --sweep, and never removes any of them.
  • Undoes any run with preen restore, and cleans up old recovery refs with preen backups --prune.

It never invents changes and never touches a commit you did not ask it to.

How grouping works

The grouping is deterministic and needs no model. preen separates dependency manifests, CI configuration, documentation, and configuration from source, then groups source by package, keeps a test file with the code it exercises, keeps rename pairs together, and treats anything you staged by hand as a boundary you drew deliberately. Dependencies are recorded first and documentation last.

Because a fixed rule cannot know whether two hunks in one file are one idea or two, the built-in grouper never splits a file.

When you want that judgment, hand grouping to a program:

preen --grouper ./my-grouper

The program reads a JSON request on stdin holding every changed file and its hunks, and writes back the commits it proposes. It can split one file's hunks across separate commits. The contract is provider agnostic, so any model CLI, script, or service wrapper can be a grouper, and none of them can touch your repository: a grouper only answers, and preen verifies every path and hunk index against the real tree before acting. If it fails, returns nothing, or names something that is not there, the run falls back to the built-in rules rather than trusting it.

Every guardrail is the same either way. The grouper chooses what goes where and nothing else.

Message style

preen writes a short imperative subject by default. Dictate the format with flags:

preen --conventional --prefix ABC-123 --max-subject 50 --no-emdash --no-semicolon

--punctuation auto reads your repository's own recent subjects and follows whatever they do. --body, --include-files, and --include-line-numbers control the message body, with line ranges read from the real hunk headers.

Or set defaults once in a .preen.toml at the repository root:

[commit]
no-emdash = trueno-semicolon = truemax-subject = 50punctuation = "never"conventional = trueprefix = "ABC-123"body = "auto"include-files = false
[run]
gate = "go test ./..."sweep = trueallow-no-verify = false
[protect]
branches = ["develop", "release/*"]

Flags beat the config file, which beats the defaults. Every generated message is checked against the style before it is recorded, so a configured convention is enforced rather than merely requested.

If your repository has hooks that block automated commits, set allow-no-verify = true under [run] to grant standing consent ahead of time. preen never bypasses a hook on its own judgment. A hook that reformats what the run commits would normally trip the conservation check; --allow-hook-rewrites accepts content differences confined to the paths the run committed.

Rewriting published history

preen will redo commits a remote already has, but only when you say so twice. --pushed grants the ask, and on a branch that is shared by name you also need --allow-protected. main, master, trunk, develop, release, and production are protected out of the box, plus anything listed under [protect] in the config, which comes from the repository and can never be dropped by a flag.

preen --pushed --pushed-base origin/main~4

The push is a third, separate confirmation, it shows you the exact command first, and it always uses --force-with-lease so it aborts rather than clobbering work that arrived after your last fetch. Consent is per invocation: the config file cannot grant it.

Commands

preen Group the working tree into commits.
preen restore [ref] Undo a run. Defaults to the most recent backup.
preen backups List recovery refs. --prune deletes the safe ones.

Exit codes are distinct, so a script can tell a rolled-back run (6, 7) from a rejected plan (5) or a declined one (8).

Undo

preen restore

This moves the branch back and returns your work to the working tree exactly as it was: same files, same content, uncommitted. It only ever accepts a preen-backup/ ref, so it cannot move your branch somewhere unrelated.

Development

go test ./...

The tests run against real git repositories in temp directories rather than a mock, because matching git's own index and patch behavior is the whole job. The conservation invariant, the published-merge guard, and the restore round trip each have their own regression test.

Claude Code plugin

The repository is also a Claude Code plugin. Add it as a marketplace and asking Claude to "clean up my commit history" loads a skill that drives the same binary, with every guardrail intact:

/plugin marketplace add dcadolph/preen
/plugin install preen@preen

More tools

  • kibble, test your README's install steps in a clean container
  • slop-chop, strip the AI tells out of your writing
  • vamoose, route time off through approval, then tell the team
  • whodar, find who to talk to about X across your work tools

License

MIT.


preen: what a bird does to put every feather back in place.

About

Split a messy working tree into clean, atomic git commits with real messages, and see the plan before anything moves. Claude Code skill and CLI.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

preen

preen

Latest releaseLicense

Clean up your commit history, automatically.

preen demo

You got in the zone and came out with forty changed files and no commits, or one giant blob with a message like wip that you are not proud of. preen turns that into a clean, ordered set of atomic commits: the shape of the history you would have written if you had committed carefully as you went.

Think git add -p and git rebase -i, done for you. preen reads everything you changed, groups it into coherent commits, writes a subject for each, orders them so the history bisects, and shows you the plan first. Nothing moves until you approve.

The built-in subjects say where, not why: Add api, Update dependencies. For messages that explain intent, hand grouping to any program you like with --grouper, a model included, and reword anything at the approval prompt.

Clean history is worth having on its own. It makes review readable, git bisect useful, and git blame honest. preen gets you there without the tedious hand-staging.

It cannot lose your work

preen only reshapes history, never content, and it enforces that rather than promising it.

Before a run it hashes a tree holding your HEAD plus every staged, unstaged, and untracked change. After the run it hashes the same thing again. The two must match exactly. If a single byte differs, the run rolls itself back to the recovery branch it made before it started and tells you which paths diverged.

Every run leaves a preen-backup/<timestamp> branch, and preen restore puts you back where you were, with your work returned to the working tree exactly as it was.

Install

go install github.com/dcadolph/preen@latest

preen is a single binary. It needs git on your PATH and nothing else: no model, no API key, no network.

Use

Run it against a dirty working tree:

preen

You get a plan like this:

Planned commits (4):
1. Update dependencies
go.mod
2. Add api
api/server.go
api/server_test.go
3. Add store
store/db.go
4. Add guide.md
docs/guide.md
Apply this plan? 4 commits [y/n, or ? for edits]:

Approve and it stages each group precisely, commits, verifies your content is unchanged, and tells you how to undo it. Or edit the plan first, one move at a time, with the full plan reshown after each:

merge 2 into 1 fold one commit into another
split 3 break a commit into one per file
move api/x.go to 2 reassign a file
reword 1 Add parser replace a subject
drop scratch.txt leave a file uncommitted
reorder 3,1,2 resequence

An edit that would stop the plan covering your tree is rejected, so the prompt cannot walk you into losing a change.

What it does

  • Surveys every uncommitted change: staged, unstaged, and untracked.
  • Groups them into atomic commits, one coherent idea each, ordered so dependencies land first and the history could be bisected.
  • Absorbs a run of unpushed commits back into the tree and redoes them clean with --absorb, no manual reset.
  • Folds dirty changes into the unpushed commits that introduced them with --fixup, then squashes them away with an autosquash rebase.
  • Refuses to redo a commit a remote already has, and moves its base forward past any merge whose side branch is published.
  • Rewrites published history only when you ask twice, with --pushed and, on a shared branch, --allow-protected, then pushes with --force-with-lease behind a separate confirmation.
  • Runs your build or test gate after each commit with --gate, rolling the whole run back on failure.
  • Preens only part of the tree with --scope, leaving the rest dirty.
  • Plans without acting with --dry-run, and skips the approval prompt with --yes for scripted runs.
  • Reports debug prints, scratch markers, commented-out code, and skipped tests with --sweep, and never removes any of them.
  • Undoes any run with preen restore, and cleans up old recovery refs with preen backups --prune.

It never invents changes and never touches a commit you did not ask it to.

How grouping works

The grouping is deterministic and needs no model. preen separates dependency manifests, CI configuration, documentation, and configuration from source, then groups source by package, keeps a test file with the code it exercises, keeps rename pairs together, and treats anything you staged by hand as a boundary you drew deliberately. Dependencies are recorded first and documentation last.

Because a fixed rule cannot know whether two hunks in one file are one idea or two, the built-in grouper never splits a file.

When you want that judgment, hand grouping to a program:

preen --grouper ./my-grouper

The program reads a JSON request on stdin holding every changed file and its hunks, and writes back the commits it proposes. It can split one file's hunks across separate commits. The contract is provider agnostic, so any model CLI, script, or service wrapper can be a grouper, and none of them can touch your repository: a grouper only answers, and preen verifies every path and hunk index against the real tree before acting. If it fails, returns nothing, or names something that is not there, the run falls back to the built-in rules rather than trusting it.

Every guardrail is the same either way. The grouper chooses what goes where and nothing else.

Message style

preen writes a short imperative subject by default. Dictate the format with flags:

preen --conventional --prefix ABC-123 --max-subject 50 --no-emdash --no-semicolon

--punctuation auto reads your repository's own recent subjects and follows whatever they do. --body, --include-files, and --include-line-numbers control the message body, with line ranges read from the real hunk headers.

Or set defaults once in a .preen.toml at the repository root:

[commit]
no-emdash = trueno-semicolon = truemax-subject = 50punctuation = "never"conventional = trueprefix = "ABC-123"body = "auto"include-files = false
[run]
gate = "go test ./..."sweep = trueallow-no-verify = false
[protect]
branches = ["develop", "release/*"]

Flags beat the config file, which beats the defaults. Every generated message is checked against the style before it is recorded, so a configured convention is enforced rather than merely requested.

If your repository has hooks that block automated commits, set allow-no-verify = true under [run] to grant standing consent ahead of time. preen never bypasses a hook on its own judgment. A hook that reformats what the run commits would normally trip the conservation check; --allow-hook-rewrites accepts content differences confined to the paths the run committed.

Rewriting published history

preen will redo commits a remote already has, but only when you say so twice. --pushed grants the ask, and on a branch that is shared by name you also need --allow-protected. main, master, trunk, develop, release, and production are protected out of the box, plus anything listed under [protect] in the config, which comes from the repository and can never be dropped by a flag.

preen --pushed --pushed-base origin/main~4

The push is a third, separate confirmation, it shows you the exact command first, and it always uses --force-with-lease so it aborts rather than clobbering work that arrived after your last fetch. Consent is per invocation: the config file cannot grant it.

Commands

preen Group the working tree into commits.
preen restore [ref] Undo a run. Defaults to the most recent backup.
preen backups List recovery refs. --prune deletes the safe ones.

Exit codes are distinct, so a script can tell a rolled-back run (6, 7) from a rejected plan (5) or a declined one (8).

Undo

preen restore

This moves the branch back and returns your work to the working tree exactly as it was: same files, same content, uncommitted. It only ever accepts a preen-backup/ ref, so it cannot move your branch somewhere unrelated.

Development

go test ./...

The tests run against real git repositories in temp directories rather than a mock, because matching git's own index and patch behavior is the whole job. The conservation invariant, the published-merge guard, and the restore round trip each have their own regression test.

Claude Code plugin

The repository is also a Claude Code plugin. Add it as a marketplace and asking Claude to "clean up my commit history" loads a skill that drives the same binary, with every guardrail intact:

/plugin marketplace add dcadolph/preen
/plugin install preen@preen

More tools

  • kibble, test your README's install steps in a clean container
  • slop-chop, strip the AI tells out of your writing
  • vamoose, route time off through approval, then tell the team
  • whodar, find who to talk to about X across your work tools

License

MIT.


preen: what a bird does to put every feather back in place.

About

Split a messy working tree into clean, atomic git commits with real messages, and see the plan before anything moves. Claude Code skill and CLI.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

preen

preen

Latest releaseLicense

Clean up your commit history, automatically.

preen demo

You got in the zone and came out with forty changed files and no commits, or one giant blob with a message like wip that you are not proud of. preen turns that into a clean, ordered set of atomic commits: the shape of the history you would have written if you had committed carefully as you went.

Think git add -p and git rebase -i, done for you. preen reads everything you changed, groups it into coherent commits, writes a subject for each, orders them so the history bisects, and shows you the plan first. Nothing moves until you approve.

The built-in subjects say where, not why: Add api, Update dependencies. For messages that explain intent, hand grouping to any program you like with --grouper, a model included, and reword anything at the approval prompt.

Clean history is worth having on its own. It makes review readable, git bisect useful, and git blame honest. preen gets you there without the tedious hand-staging.

It cannot lose your work

preen only reshapes history, never content, and it enforces that rather than promising it.

Before a run it hashes a tree holding your HEAD plus every staged, unstaged, and untracked change. After the run it hashes the same thing again. The two must match exactly. If a single byte differs, the run rolls itself back to the recovery branch it made before it started and tells you which paths diverged.

Every run leaves a preen-backup/<timestamp> branch, and preen restore puts you back where you were, with your work returned to the working tree exactly as it was.

Install

go install github.com/dcadolph/preen@latest

preen is a single binary. It needs git on your PATH and nothing else: no model, no API key, no network.

Use

Run it against a dirty working tree:

preen

You get a plan like this:

Planned commits (4):
1. Update dependencies
go.mod
2. Add api
api/server.go
api/server_test.go
3. Add store
store/db.go
4. Add guide.md
docs/guide.md
Apply this plan? 4 commits [y/n, or ? for edits]:

Approve and it stages each group precisely, commits, verifies your content is unchanged, and tells you how to undo it. Or edit the plan first, one move at a time, with the full plan reshown after each:

merge 2 into 1 fold one commit into another
split 3 break a commit into one per file
move api/x.go to 2 reassign a file
reword 1 Add parser replace a subject
drop scratch.txt leave a file uncommitted
reorder 3,1,2 resequence

An edit that would stop the plan covering your tree is rejected, so the prompt cannot walk you into losing a change.

What it does

  • Surveys every uncommitted change: staged, unstaged, and untracked.
  • Groups them into atomic commits, one coherent idea each, ordered so dependencies land first and the history could be bisected.
  • Absorbs a run of unpushed commits back into the tree and redoes them clean with --absorb, no manual reset.
  • Folds dirty changes into the unpushed commits that introduced them with --fixup, then squashes them away with an autosquash rebase.
  • Refuses to redo a commit a remote already has, and moves its base forward past any merge whose side branch is published.
  • Rewrites published history only when you ask twice, with --pushed and, on a shared branch, --allow-protected, then pushes with --force-with-lease behind a separate confirmation.
  • Runs your build or test gate after each commit with --gate, rolling the whole run back on failure.
  • Preens only part of the tree with --scope, leaving the rest dirty.
  • Plans without acting with --dry-run, and skips the approval prompt with --yes for scripted runs.
  • Reports debug prints, scratch markers, commented-out code, and skipped tests with --sweep, and never removes any of them.
  • Undoes any run with preen restore, and cleans up old recovery refs with preen backups --prune.

It never invents changes and never touches a commit you did not ask it to.

How grouping works

The grouping is deterministic and needs no model. preen separates dependency manifests, CI configuration, documentation, and configuration from source, then groups source by package, keeps a test file with the code it exercises, keeps rename pairs together, and treats anything you staged by hand as a boundary you drew deliberately. Dependencies are recorded first and documentation last.

Because a fixed rule cannot know whether two hunks in one file are one idea or two, the built-in grouper never splits a file.

When you want that judgment, hand grouping to a program:

preen --grouper ./my-grouper

The program reads a JSON request on stdin holding every changed file and its hunks, and writes back the commits it proposes. It can split one file's hunks across separate commits. The contract is provider agnostic, so any model CLI, script, or service wrapper can be a grouper, and none of them can touch your repository: a grouper only answers, and preen verifies every path and hunk index against the real tree before acting. If it fails, returns nothing, or names something that is not there, the run falls back to the built-in rules rather than trusting it.

Every guardrail is the same either way. The grouper chooses what goes where and nothing else.

Message style

preen writes a short imperative subject by default. Dictate the format with flags:

preen --conventional --prefix ABC-123 --max-subject 50 --no-emdash --no-semicolon

--punctuation auto reads your repository's own recent subjects and follows whatever they do. --body, --include-files, and --include-line-numbers control the message body, with line ranges read from the real hunk headers.

Or set defaults once in a .preen.toml at the repository root:

[commit]
no-emdash = trueno-semicolon = truemax-subject = 50punctuation = "never"conventional = trueprefix = "ABC-123"body = "auto"include-files = false
[run]
gate = "go test ./..."sweep = trueallow-no-verify = false
[protect]
branches = ["develop", "release/*"]

Flags beat the config file, which beats the defaults. Every generated message is checked against the style before it is recorded, so a configured convention is enforced rather than merely requested.

If your repository has hooks that block automated commits, set allow-no-verify = true under [run] to grant standing consent ahead of time. preen never bypasses a hook on its own judgment. A hook that reformats what the run commits would normally trip the conservation check; --allow-hook-rewrites accepts content differences confined to the paths the run committed.

Rewriting published history

preen will redo commits a remote already has, but only when you say so twice. --pushed grants the ask, and on a branch that is shared by name you also need --allow-protected. main, master, trunk, develop, release, and production are protected out of the box, plus anything listed under [protect] in the config, which comes from the repository and can never be dropped by a flag.

preen --pushed --pushed-base origin/main~4

The push is a third, separate confirmation, it shows you the exact command first, and it always uses --force-with-lease so it aborts rather than clobbering work that arrived after your last fetch. Consent is per invocation: the config file cannot grant it.

Commands

preen Group the working tree into commits.
preen restore [ref] Undo a run. Defaults to the most recent backup.
preen backups List recovery refs. --prune deletes the safe ones.

Exit codes are distinct, so a script can tell a rolled-back run (6, 7) from a rejected plan (5) or a declined one (8).

Undo

preen restore

This moves the branch back and returns your work to the working tree exactly as it was: same files, same content, uncommitted. It only ever accepts a preen-backup/ ref, so it cannot move your branch somewhere unrelated.

Development

go test ./...

The tests run against real git repositories in temp directories rather than a mock, because matching git's own index and patch behavior is the whole job. The conservation invariant, the published-merge guard, and the restore round trip each have their own regression test.

Claude Code plugin

The repository is also a Claude Code plugin. Add it as a marketplace and asking Claude to "clean up my commit history" loads a skill that drives the same binary, with every guardrail intact:

/plugin marketplace add dcadolph/preen
/plugin install preen@preen

More tools

  • kibble, test your README's install steps in a clean container
  • slop-chop, strip the AI tells out of your writing
  • vamoose, route time off through approval, then tell the team
  • whodar, find who to talk to about X across your work tools

License

MIT.


preen: what a bird does to put every feather back in place.

About

Split a messy working tree into clean, atomic git commits with real messages, and see the plan before anything moves. Claude Code skill and CLI.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

preen

preen

Latest releaseLicense

Clean up your commit history, automatically.

preen demo

You got in the zone and came out with forty changed files and no commits, or one giant blob with a message like wip that you are not proud of. preen turns that into a clean, ordered set of atomic commits: the shape of the history you would have written if you had committed carefully as you went.

Think git add -p and git rebase -i, done for you. preen reads everything you changed, groups it into coherent commits, writes a subject for each, orders them so the history bisects, and shows you the plan first. Nothing moves until you approve.

The built-in subjects say where, not why: Add api, Update dependencies. For messages that explain intent, hand grouping to any program you like with --grouper, a model included, and reword anything at the approval prompt.

Clean history is worth having on its own. It makes review readable, git bisect useful, and git blame honest. preen gets you there without the tedious hand-staging.

It cannot lose your work

preen only reshapes history, never content, and it enforces that rather than promising it.

Before a run it hashes a tree holding your HEAD plus every staged, unstaged, and untracked change. After the run it hashes the same thing again. The two must match exactly. If a single byte differs, the run rolls itself back to the recovery branch it made before it started and tells you which paths diverged.

Every run leaves a preen-backup/<timestamp> branch, and preen restore puts you back where you were, with your work returned to the working tree exactly as it was.

Install

go install github.com/dcadolph/preen@latest

preen is a single binary. It needs git on your PATH and nothing else: no model, no API key, no network.

Use

Run it against a dirty working tree:

preen

You get a plan like this:

Planned commits (4):
1. Update dependencies
go.mod
2. Add api
api/server.go
api/server_test.go
3. Add store
store/db.go
4. Add guide.md
docs/guide.md
Apply this plan? 4 commits [y/n, or ? for edits]:

Approve and it stages each group precisely, commits, verifies your content is unchanged, and tells you how to undo it. Or edit the plan first, one move at a time, with the full plan reshown after each:

merge 2 into 1 fold one commit into another
split 3 break a commit into one per file
move api/x.go to 2 reassign a file
reword 1 Add parser replace a subject
drop scratch.txt leave a file uncommitted
reorder 3,1,2 resequence

An edit that would stop the plan covering your tree is rejected, so the prompt cannot walk you into losing a change.

What it does

  • Surveys every uncommitted change: staged, unstaged, and untracked.
  • Groups them into atomic commits, one coherent idea each, ordered so dependencies land first and the history could be bisected.
  • Absorbs a run of unpushed commits back into the tree and redoes them clean with --absorb, no manual reset.
  • Folds dirty changes into the unpushed commits that introduced them with --fixup, then squashes them away with an autosquash rebase.
  • Refuses to redo a commit a remote already has, and moves its base forward past any merge whose side branch is published.
  • Rewrites published history only when you ask twice, with --pushed and, on a shared branch, --allow-protected, then pushes with --force-with-lease behind a separate confirmation.
  • Runs your build or test gate after each commit with --gate, rolling the whole run back on failure.
  • Preens only part of the tree with --scope, leaving the rest dirty.
  • Plans without acting with --dry-run, and skips the approval prompt with --yes for scripted runs.
  • Reports debug prints, scratch markers, commented-out code, and skipped tests with --sweep, and never removes any of them.
  • Undoes any run with preen restore, and cleans up old recovery refs with preen backups --prune.

It never invents changes and never touches a commit you did not ask it to.

How grouping works

The grouping is deterministic and needs no model. preen separates dependency manifests, CI configuration, documentation, and configuration from source, then groups source by package, keeps a test file with the code it exercises, keeps rename pairs together, and treats anything you staged by hand as a boundary you drew deliberately. Dependencies are recorded first and documentation last.

Because a fixed rule cannot know whether two hunks in one file are one idea or two, the built-in grouper never splits a file.

When you want that judgment, hand grouping to a program:

preen --grouper ./my-grouper

The program reads a JSON request on stdin holding every changed file and its hunks, and writes back the commits it proposes. It can split one file's hunks across separate commits. The contract is provider agnostic, so any model CLI, script, or service wrapper can be a grouper, and none of them can touch your repository: a grouper only answers, and preen verifies every path and hunk index against the real tree before acting. If it fails, returns nothing, or names something that is not there, the run falls back to the built-in rules rather than trusting it.

Every guardrail is the same either way. The grouper chooses what goes where and nothing else.

Message style

preen writes a short imperative subject by default. Dictate the format with flags:

preen --conventional --prefix ABC-123 --max-subject 50 --no-emdash --no-semicolon

--punctuation auto reads your repository's own recent subjects and follows whatever they do. --body, --include-files, and --include-line-numbers control the message body, with line ranges read from the real hunk headers.

Or set defaults once in a .preen.toml at the repository root:

[commit]
no-emdash = trueno-semicolon = truemax-subject = 50punctuation = "never"conventional = trueprefix = "ABC-123"body = "auto"include-files = false
[run]
gate = "go test ./..."sweep = trueallow-no-verify = false
[protect]
branches = ["develop", "release/*"]

Flags beat the config file, which beats the defaults. Every generated message is checked against the style before it is recorded, so a configured convention is enforced rather than merely requested.

If your repository has hooks that block automated commits, set allow-no-verify = true under [run] to grant standing consent ahead of time. preen never bypasses a hook on its own judgment. A hook that reformats what the run commits would normally trip the conservation check; --allow-hook-rewrites accepts content differences confined to the paths the run committed.

Rewriting published history

preen will redo commits a remote already has, but only when you say so twice. --pushed grants the ask, and on a branch that is shared by name you also need --allow-protected. main, master, trunk, develop, release, and production are protected out of the box, plus anything listed under [protect] in the config, which comes from the repository and can never be dropped by a flag.

preen --pushed --pushed-base origin/main~4

The push is a third, separate confirmation, it shows you the exact command first, and it always uses --force-with-lease so it aborts rather than clobbering work that arrived after your last fetch. Consent is per invocation: the config file cannot grant it.

Commands

preen Group the working tree into commits.
preen restore [ref] Undo a run. Defaults to the most recent backup.
preen backups List recovery refs. --prune deletes the safe ones.

Exit codes are distinct, so a script can tell a rolled-back run (6, 7) from a rejected plan (5) or a declined one (8).

Undo

preen restore

This moves the branch back and returns your work to the working tree exactly as it was: same files, same content, uncommitted. It only ever accepts a preen-backup/ ref, so it cannot move your branch somewhere unrelated.

Development

go test ./...

The tests run against real git repositories in temp directories rather than a mock, because matching git's own index and patch behavior is the whole job. The conservation invariant, the published-merge guard, and the restore round trip each have their own regression test.

Claude Code plugin

The repository is also a Claude Code plugin. Add it as a marketplace and asking Claude to "clean up my commit history" loads a skill that drives the same binary, with every guardrail intact:

/plugin marketplace add dcadolph/preen
/plugin install preen@preen

More tools

  • kibble, test your README's install steps in a clean container
  • slop-chop, strip the AI tells out of your writing
  • vamoose, route time off through approval, then tell the team
  • whodar, find who to talk to about X across your work tools

License

MIT.


preen: what a bird does to put every feather back in place.

About

Split a messy working tree into clean, atomic git commits with real messages, and see the plan before anything moves. Claude Code skill and CLI.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

preen

preen

Latest releaseLicense

Clean up your commit history, automatically.

preen demo

You got in the zone and came out with forty changed files and no commits, or one giant blob with a message like wip that you are not proud of. preen turns that into a clean, ordered set of atomic commits: the shape of the history you would have written if you had committed carefully as you went.

Think git add -p and git rebase -i, done for you. preen reads everything you changed, groups it into coherent commits, writes a subject for each, orders them so the history bisects, and shows you the plan first. Nothing moves until you approve.

The built-in subjects say where, not why: Add api, Update dependencies. For messages that explain intent, hand grouping to any program you like with --grouper, a model included, and reword anything at the approval prompt.

Clean history is worth having on its own. It makes review readable, git bisect useful, and git blame honest. preen gets you there without the tedious hand-staging.

It cannot lose your work

preen only reshapes history, never content, and it enforces that rather than promising it.

Before a run it hashes a tree holding your HEAD plus every staged, unstaged, and untracked change. After the run it hashes the same thing again. The two must match exactly. If a single byte differs, the run rolls itself back to the recovery branch it made before it started and tells you which paths diverged.

Every run leaves a preen-backup/<timestamp> branch, and preen restore puts you back where you were, with your work returned to the working tree exactly as it was.

Install

go install github.com/dcadolph/preen@latest

preen is a single binary. It needs git on your PATH and nothing else: no model, no API key, no network.

Use

Run it against a dirty working tree:

preen

You get a plan like this:

Planned commits (4):
1. Update dependencies
go.mod
2. Add api
api/server.go
api/server_test.go
3. Add store
store/db.go
4. Add guide.md
docs/guide.md
Apply this plan? 4 commits [y/n, or ? for edits]:

Approve and it stages each group precisely, commits, verifies your content is unchanged, and tells you how to undo it. Or edit the plan first, one move at a time, with the full plan reshown after each:

merge 2 into 1 fold one commit into another
split 3 break a commit into one per file
move api/x.go to 2 reassign a file
reword 1 Add parser replace a subject
drop scratch.txt leave a file uncommitted
reorder 3,1,2 resequence

An edit that would stop the plan covering your tree is rejected, so the prompt cannot walk you into losing a change.

What it does

  • Surveys every uncommitted change: staged, unstaged, and untracked.
  • Groups them into atomic commits, one coherent idea each, ordered so dependencies land first and the history could be bisected.
  • Absorbs a run of unpushed commits back into the tree and redoes them clean with --absorb, no manual reset.
  • Folds dirty changes into the unpushed commits that introduced them with --fixup, then squashes them away with an autosquash rebase.
  • Refuses to redo a commit a remote already has, and moves its base forward past any merge whose side branch is published.
  • Rewrites published history only when you ask twice, with --pushed and, on a shared branch, --allow-protected, then pushes with --force-with-lease behind a separate confirmation.
  • Runs your build or test gate after each commit with --gate, rolling the whole run back on failure.
  • Preens only part of the tree with --scope, leaving the rest dirty.
  • Plans without acting with --dry-run, and skips the approval prompt with --yes for scripted runs.
  • Reports debug prints, scratch markers, commented-out code, and skipped tests with --sweep, and never removes any of them.
  • Undoes any run with preen restore, and cleans up old recovery refs with preen backups --prune.

It never invents changes and never touches a commit you did not ask it to.

How grouping works

The grouping is deterministic and needs no model. preen separates dependency manifests, CI configuration, documentation, and configuration from source, then groups source by package, keeps a test file with the code it exercises, keeps rename pairs together, and treats anything you staged by hand as a boundary you drew deliberately. Dependencies are recorded first and documentation last.

Because a fixed rule cannot know whether two hunks in one file are one idea or two, the built-in grouper never splits a file.

When you want that judgment, hand grouping to a program:

preen --grouper ./my-grouper

The program reads a JSON request on stdin holding every changed file and its hunks, and writes back the commits it proposes. It can split one file's hunks across separate commits. The contract is provider agnostic, so any model CLI, script, or service wrapper can be a grouper, and none of them can touch your repository: a grouper only answers, and preen verifies every path and hunk index against the real tree before acting. If it fails, returns nothing, or names something that is not there, the run falls back to the built-in rules rather than trusting it.

Every guardrail is the same either way. The grouper chooses what goes where and nothing else.

Message style

preen writes a short imperative subject by default. Dictate the format with flags:

preen --conventional --prefix ABC-123 --max-subject 50 --no-emdash --no-semicolon

--punctuation auto reads your repository's own recent subjects and follows whatever they do. --body, --include-files, and --include-line-numbers control the message body, with line ranges read from the real hunk headers.

Or set defaults once in a .preen.toml at the repository root:

[commit]
no-emdash = trueno-semicolon = truemax-subject = 50punctuation = "never"conventional = trueprefix = "ABC-123"body = "auto"include-files = false
[run]
gate = "go test ./..."sweep = trueallow-no-verify = false
[protect]
branches = ["develop", "release/*"]

Flags beat the config file, which beats the defaults. Every generated message is checked against the style before it is recorded, so a configured convention is enforced rather than merely requested.

If your repository has hooks that block automated commits, set allow-no-verify = true under [run] to grant standing consent ahead of time. preen never bypasses a hook on its own judgment. A hook that reformats what the run commits would normally trip the conservation check; --allow-hook-rewrites accepts content differences confined to the paths the run committed.

Rewriting published history

preen will redo commits a remote already has, but only when you say so twice. --pushed grants the ask, and on a branch that is shared by name you also need --allow-protected. main, master, trunk, develop, release, and production are protected out of the box, plus anything listed under [protect] in the config, which comes from the repository and can never be dropped by a flag.

preen --pushed --pushed-base origin/main~4

The push is a third, separate confirmation, it shows you the exact command first, and it always uses --force-with-lease so it aborts rather than clobbering work that arrived after your last fetch. Consent is per invocation: the config file cannot grant it.

Commands

preen Group the working tree into commits.
preen restore [ref] Undo a run. Defaults to the most recent backup.
preen backups List recovery refs. --prune deletes the safe ones.

Exit codes are distinct, so a script can tell a rolled-back run (6, 7) from a rejected plan (5) or a declined one (8).

Undo

preen restore

This moves the branch back and returns your work to the working tree exactly as it was: same files, same content, uncommitted. It only ever accepts a preen-backup/ ref, so it cannot move your branch somewhere unrelated.

Development

go test ./...

The tests run against real git repositories in temp directories rather than a mock, because matching git's own index and patch behavior is the whole job. The conservation invariant, the published-merge guard, and the restore round trip each have their own regression test.

Claude Code plugin

The repository is also a Claude Code plugin. Add it as a marketplace and asking Claude to "clean up my commit history" loads a skill that drives the same binary, with every guardrail intact:

/plugin marketplace add dcadolph/preen
/plugin install preen@preen

More tools

  • kibble, test your README's install steps in a clean container
  • slop-chop, strip the AI tells out of your writing
  • vamoose, route time off through approval, then tell the team
  • whodar, find who to talk to about X across your work tools

License

MIT.


preen: what a bird does to put every feather back in place.

About

Split a messy working tree into clean, atomic git commits with real messages, and see the plan before anything moves. Claude Code skill and CLI.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

preen

preen

Latest releaseLicense

Clean up your commit history, automatically.

preen demo

You got in the zone and came out with forty changed files and no commits, or one giant blob with a message like wip that you are not proud of. preen turns that into a clean, ordered set of atomic commits: the shape of the history you would have written if you had committed carefully as you went.

Think git add -p and git rebase -i, done for you. preen reads everything you changed, groups it into coherent commits, writes a subject for each, orders them so the history bisects, and shows you the plan first. Nothing moves until you approve.

The built-in subjects say where, not why: Add api, Update dependencies. For messages that explain intent, hand grouping to any program you like with --grouper, a model included, and reword anything at the approval prompt.

Clean history is worth having on its own. It makes review readable, git bisect useful, and git blame honest. preen gets you there without the tedious hand-staging.

It cannot lose your work

preen only reshapes history, never content, and it enforces that rather than promising it.

Before a run it hashes a tree holding your HEAD plus every staged, unstaged, and untracked change. After the run it hashes the same thing again. The two must match exactly. If a single byte differs, the run rolls itself back to the recovery branch it made before it started and tells you which paths diverged.

Every run leaves a preen-backup/<timestamp> branch, and preen restore puts you back where you were, with your work returned to the working tree exactly as it was.

Install

go install github.com/dcadolph/preen@latest

preen is a single binary. It needs git on your PATH and nothing else: no model, no API key, no network.

Use

Run it against a dirty working tree:

preen

You get a plan like this:

Planned commits (4):
1. Update dependencies
go.mod
2. Add api
api/server.go
api/server_test.go
3. Add store
store/db.go
4. Add guide.md
docs/guide.md
Apply this plan? 4 commits [y/n, or ? for edits]:

Approve and it stages each group precisely, commits, verifies your content is unchanged, and tells you how to undo it. Or edit the plan first, one move at a time, with the full plan reshown after each:

merge 2 into 1 fold one commit into another
split 3 break a commit into one per file
move api/x.go to 2 reassign a file
reword 1 Add parser replace a subject
drop scratch.txt leave a file uncommitted
reorder 3,1,2 resequence

An edit that would stop the plan covering your tree is rejected, so the prompt cannot walk you into losing a change.

What it does

  • Surveys every uncommitted change: staged, unstaged, and untracked.
  • Groups them into atomic commits, one coherent idea each, ordered so dependencies land first and the history could be bisected.
  • Absorbs a run of unpushed commits back into the tree and redoes them clean with --absorb, no manual reset.
  • Folds dirty changes into the unpushed commits that introduced them with --fixup, then squashes them away with an autosquash rebase.
  • Refuses to redo a commit a remote already has, and moves its base forward past any merge whose side branch is published.
  • Rewrites published history only when you ask twice, with --pushed and, on a shared branch, --allow-protected, then pushes with --force-with-lease behind a separate confirmation.
  • Runs your build or test gate after each commit with --gate, rolling the whole run back on failure.
  • Preens only part of the tree with --scope, leaving the rest dirty.
  • Plans without acting with --dry-run, and skips the approval prompt with --yes for scripted runs.
  • Reports debug prints, scratch markers, commented-out code, and skipped tests with --sweep, and never removes any of them.
  • Undoes any run with preen restore, and cleans up old recovery refs with preen backups --prune.

It never invents changes and never touches a commit you did not ask it to.

How grouping works

The grouping is deterministic and needs no model. preen separates dependency manifests, CI configuration, documentation, and configuration from source, then groups source by package, keeps a test file with the code it exercises, keeps rename pairs together, and treats anything you staged by hand as a boundary you drew deliberately. Dependencies are recorded first and documentation last.

Because a fixed rule cannot know whether two hunks in one file are one idea or two, the built-in grouper never splits a file.

When you want that judgment, hand grouping to a program:

preen --grouper ./my-grouper

The program reads a JSON request on stdin holding every changed file and its hunks, and writes back the commits it proposes. It can split one file's hunks across separate commits. The contract is provider agnostic, so any model CLI, script, or service wrapper can be a grouper, and none of them can touch your repository: a grouper only answers, and preen verifies every path and hunk index against the real tree before acting. If it fails, returns nothing, or names something that is not there, the run falls back to the built-in rules rather than trusting it.

Every guardrail is the same either way. The grouper chooses what goes where and nothing else.

Message style

preen writes a short imperative subject by default. Dictate the format with flags:

preen --conventional --prefix ABC-123 --max-subject 50 --no-emdash --no-semicolon

--punctuation auto reads your repository's own recent subjects and follows whatever they do. --body, --include-files, and --include-line-numbers control the message body, with line ranges read from the real hunk headers.

Or set defaults once in a .preen.toml at the repository root:

[commit]
no-emdash = trueno-semicolon = truemax-subject = 50punctuation = "never"conventional = trueprefix = "ABC-123"body = "auto"include-files = false
[run]
gate = "go test ./..."sweep = trueallow-no-verify = false
[protect]
branches = ["develop", "release/*"]

Flags beat the config file, which beats the defaults. Every generated message is checked against the style before it is recorded, so a configured convention is enforced rather than merely requested.

If your repository has hooks that block automated commits, set allow-no-verify = true under [run] to grant standing consent ahead of time. preen never bypasses a hook on its own judgment. A hook that reformats what the run commits would normally trip the conservation check; --allow-hook-rewrites accepts content differences confined to the paths the run committed.

Rewriting published history

preen will redo commits a remote already has, but only when you say so twice. --pushed grants the ask, and on a branch that is shared by name you also need --allow-protected. main, master, trunk, develop, release, and production are protected out of the box, plus anything listed under [protect] in the config, which comes from the repository and can never be dropped by a flag.

preen --pushed --pushed-base origin/main~4

The push is a third, separate confirmation, it shows you the exact command first, and it always uses --force-with-lease so it aborts rather than clobbering work that arrived after your last fetch. Consent is per invocation: the config file cannot grant it.

Commands

preen Group the working tree into commits.
preen restore [ref] Undo a run. Defaults to the most recent backup.
preen backups List recovery refs. --prune deletes the safe ones.

Exit codes are distinct, so a script can tell a rolled-back run (6, 7) from a rejected plan (5) or a declined one (8).

Undo

preen restore

This moves the branch back and returns your work to the working tree exactly as it was: same files, same content, uncommitted. It only ever accepts a preen-backup/ ref, so it cannot move your branch somewhere unrelated.

Development

go test ./...

The tests run against real git repositories in temp directories rather than a mock, because matching git's own index and patch behavior is the whole job. The conservation invariant, the published-merge guard, and the restore round trip each have their own regression test.

Claude Code plugin

The repository is also a Claude Code plugin. Add it as a marketplace and asking Claude to "clean up my commit history" loads a skill that drives the same binary, with every guardrail intact:

/plugin marketplace add dcadolph/preen
/plugin install preen@preen

More tools

  • kibble, test your README's install steps in a clean container
  • slop-chop, strip the AI tells out of your writing
  • vamoose, route time off through approval, then tell the team
  • whodar, find who to talk to about X across your work tools

License

MIT.


preen: what a bird does to put every feather back in place.

About

Split a messy working tree into clean, atomic git commits with real messages, and see the plan before anything moves. Claude Code skill and CLI.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

preen

preen

Latest releaseLicense

Clean up your commit history, automatically.

preen demo

You got in the zone and came out with forty changed files and no commits, or one giant blob with a message like wip that you are not proud of. preen turns that into a clean, ordered set of atomic commits: the shape of the history you would have written if you had committed carefully as you went.

Think git add -p and git rebase -i, done for you. preen reads everything you changed, groups it into coherent commits, writes a subject for each, orders them so the history bisects, and shows you the plan first. Nothing moves until you approve.

The built-in subjects say where, not why: Add api, Update dependencies. For messages that explain intent, hand grouping to any program you like with --grouper, a model included, and reword anything at the approval prompt.

Clean history is worth having on its own. It makes review readable, git bisect useful, and git blame honest. preen gets you there without the tedious hand-staging.

It cannot lose your work

preen only reshapes history, never content, and it enforces that rather than promising it.

Before a run it hashes a tree holding your HEAD plus every staged, unstaged, and untracked change. After the run it hashes the same thing again. The two must match exactly. If a single byte differs, the run rolls itself back to the recovery branch it made before it started and tells you which paths diverged.

Every run leaves a preen-backup/<timestamp> branch, and preen restore puts you back where you were, with your work returned to the working tree exactly as it was.

Install

go install github.com/dcadolph/preen@latest

preen is a single binary. It needs git on your PATH and nothing else: no model, no API key, no network.

Use

Run it against a dirty working tree:

preen

You get a plan like this:

Planned commits (4):
1. Update dependencies
go.mod
2. Add api
api/server.go
api/server_test.go
3. Add store
store/db.go
4. Add guide.md
docs/guide.md
Apply this plan? 4 commits [y/n, or ? for edits]:

Approve and it stages each group precisely, commits, verifies your content is unchanged, and tells you how to undo it. Or edit the plan first, one move at a time, with the full plan reshown after each:

merge 2 into 1 fold one commit into another
split 3 break a commit into one per file
move api/x.go to 2 reassign a file
reword 1 Add parser replace a subject
drop scratch.txt leave a file uncommitted
reorder 3,1,2 resequence

An edit that would stop the plan covering your tree is rejected, so the prompt cannot walk you into losing a change.

What it does

  • Surveys every uncommitted change: staged, unstaged, and untracked.
  • Groups them into atomic commits, one coherent idea each, ordered so dependencies land first and the history could be bisected.
  • Absorbs a run of unpushed commits back into the tree and redoes them clean with --absorb, no manual reset.
  • Folds dirty changes into the unpushed commits that introduced them with --fixup, then squashes them away with an autosquash rebase.
  • Refuses to redo a commit a remote already has, and moves its base forward past any merge whose side branch is published.
  • Rewrites published history only when you ask twice, with --pushed and, on a shared branch, --allow-protected, then pushes with --force-with-lease behind a separate confirmation.
  • Runs your build or test gate after each commit with --gate, rolling the whole run back on failure.
  • Preens only part of the tree with --scope, leaving the rest dirty.
  • Plans without acting with --dry-run, and skips the approval prompt with --yes for scripted runs.
  • Reports debug prints, scratch markers, commented-out code, and skipped tests with --sweep, and never removes any of them.
  • Undoes any run with preen restore, and cleans up old recovery refs with preen backups --prune.

It never invents changes and never touches a commit you did not ask it to.

How grouping works

The grouping is deterministic and needs no model. preen separates dependency manifests, CI configuration, documentation, and configuration from source, then groups source by package, keeps a test file with the code it exercises, keeps rename pairs together, and treats anything you staged by hand as a boundary you drew deliberately. Dependencies are recorded first and documentation last.

Because a fixed rule cannot know whether two hunks in one file are one idea or two, the built-in grouper never splits a file.

When you want that judgment, hand grouping to a program:

preen --grouper ./my-grouper

The program reads a JSON request on stdin holding every changed file and its hunks, and writes back the commits it proposes. It can split one file's hunks across separate commits. The contract is provider agnostic, so any model CLI, script, or service wrapper can be a grouper, and none of them can touch your repository: a grouper only answers, and preen verifies every path and hunk index against the real tree before acting. If it fails, returns nothing, or names something that is not there, the run falls back to the built-in rules rather than trusting it.

Every guardrail is the same either way. The grouper chooses what goes where and nothing else.

Message style

preen writes a short imperative subject by default. Dictate the format with flags:

preen --conventional --prefix ABC-123 --max-subject 50 --no-emdash --no-semicolon

--punctuation auto reads your repository's own recent subjects and follows whatever they do. --body, --include-files, and --include-line-numbers control the message body, with line ranges read from the real hunk headers.

Or set defaults once in a .preen.toml at the repository root:

[commit]
no-emdash = trueno-semicolon = truemax-subject = 50punctuation = "never"conventional = trueprefix = "ABC-123"body = "auto"include-files = false
[run]
gate = "go test ./..."sweep = trueallow-no-verify = false
[protect]
branches = ["develop", "release/*"]

Flags beat the config file, which beats the defaults. Every generated message is checked against the style before it is recorded, so a configured convention is enforced rather than merely requested.

If your repository has hooks that block automated commits, set allow-no-verify = true under [run] to grant standing consent ahead of time. preen never bypasses a hook on its own judgment. A hook that reformats what the run commits would normally trip the conservation check; --allow-hook-rewrites accepts content differences confined to the paths the run committed.

Rewriting published history

preen will redo commits a remote already has, but only when you say so twice. --pushed grants the ask, and on a branch that is shared by name you also need --allow-protected. main, master, trunk, develop, release, and production are protected out of the box, plus anything listed under [protect] in the config, which comes from the repository and can never be dropped by a flag.

preen --pushed --pushed-base origin/main~4

The push is a third, separate confirmation, it shows you the exact command first, and it always uses --force-with-lease so it aborts rather than clobbering work that arrived after your last fetch. Consent is per invocation: the config file cannot grant it.

Commands

preen Group the working tree into commits.
preen restore [ref] Undo a run. Defaults to the most recent backup.
preen backups List recovery refs. --prune deletes the safe ones.

Exit codes are distinct, so a script can tell a rolled-back run (6, 7) from a rejected plan (5) or a declined one (8).

Undo

preen restore

This moves the branch back and returns your work to the working tree exactly as it was: same files, same content, uncommitted. It only ever accepts a preen-backup/ ref, so it cannot move your branch somewhere unrelated.

Development

go test ./...

The tests run against real git repositories in temp directories rather than a mock, because matching git's own index and patch behavior is the whole job. The conservation invariant, the published-merge guard, and the restore round trip each have their own regression test.

Claude Code plugin

The repository is also a Claude Code plugin. Add it as a marketplace and asking Claude to "clean up my commit history" loads a skill that drives the same binary, with every guardrail intact:

/plugin marketplace add dcadolph/preen
/plugin install preen@preen

More tools

  • kibble, test your README's install steps in a clean container
  • slop-chop, strip the AI tells out of your writing
  • vamoose, route time off through approval, then tell the team
  • whodar, find who to talk to about X across your work tools

License

MIT.


preen: what a bird does to put every feather back in place.

About

Split a messy working tree into clean, atomic git commits with real messages, and see the plan before anything moves. Claude Code skill and CLI.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages