On this page
#Changelog
#0.26.0
Each confirmation now owns its consent flag, hook run stops pretending to have a dry run, and the command surface is pinned by tests.
Context
This release closes the residue the strictcli effects-regime migration left behind. Two confirmation layers had ended up in series: the framework asks before a consequential command runs, and safegit asked again behind it. A human answered twice; a script that passed --approve-consequential had every safegit-side question answered for it, including the one that flag says nothing about -- whether the remote a backup is about to land on is public. Each question now carries its own flag. --allow-public-remote is the only answer to the exposure question, which restores the property that a non-interactive run cannot publish a branch to a public repository without having said so, and the four history rewrites lost their duplicate prompt in favour of a notice.
hook run was found to ignore --dry-run entirely and execute every installed script for real. It now declares that it has no dry-run mode, with the reason, rather than inventing a preview of a program safegit did not write.
Under the hood, main() was split so that a test can hold the registered app: every command's classification, its consequential and dry-run declarations and the group tree are now pinned by a table, and the app description's command count is checked against the number of commands actually registered -- it had been advertising twenty against thirty-one. The generated selfdoc manifest and content hashes are tracked in the repo, matching the rest of the fleet.
#Breaking
- **Breaking:
--approve-consequentialno longer consents to backing up to a public remote;--allow-public-remotedoes.** Two confirmations sat in series: the framework asks before a consequential command runs, and safegit asked again behind it. An operator at a terminal answered twice, and a script that passed--approve-consequentialhad every safegit-side question answered for it -- including the one that flag says nothing about, whether the remote a backup lands on is public. Each question now owns its flag.safegit backup backupgains--allow-public-remote, which is the only thing that consents to a public or unclassifiable target, so a non-interactive run can no longer publish a branch without having said so. The four history rewrites (scrub file/match/run,author rewrite) lost their second prompt: the framework already took consent, so their commit counts and scope are printed as a notice instead of asked again.doctor --uninstallis unchanged.
#Fixes
- **
safegit --dry-run hook runno longer runs the hooks.** The command ignored--dry-runoutright, so what was meant as a preview executed every installed pre-pre-push script for real. There is nothing honest to preview here -- a hook is a script you supplied, and safegit cannot know what it does -- sohook runnow declares that it has no dry-run mode and the flag is refused with that reason.safegit hook listshows which scripts a push would run. - The README no longer documents a flag that does not exist. Its note on PID reuse told you to run
safegit unlock --force, which has never been a valid invocation --unlocktakes a ref and no flags -- so anyone following it got a parse error. It now describes whatunlockdoes, including that it refuses to clear a lock whose holder is still alive. The commands and integration guides were updated for the new per-condition consent flags, andhook rungained a section on why it refuses--dry-run.
#0.25.1
Publishes 0.25.0's platform binaries and container image, which a new source-wide secret scan had blocked
Context
0.25.0 shipped as a Go module and nothing else. The re-scaffolded publish workflow scans the whole source tree with gitleaks before it builds anything, and safegit's scrub and scan tests plant strings that look precisely like leaked credentials -- SECRET_XYZ_123, sk_live_abc123 and friends -- because a scrubber that is never given anything to scrub is not being tested. The scan found five, both publish jobs failed, and the 0.25.0 Release ended up with no binaries attached and no image in the container registry.
The repository simply had no .gitleaks.toml; the scan step is new. The allowlist added here names the four fixture literals rather than exempting the test files wholesale, so if a real credential is ever pasted into one of those files the scan still catches it.
This is a fix-forward rather than a retag: the tag on 0.25.0 sits on a tree the scan will reject on every re-run, so there is no dispatch that could make it publish. Nothing about 0.25.0 is withdrawn -- the Go module proxy has served it since it was tagged, and this release changes no code.
#Fixes
- 0.25.0's binaries and container image are published. The publish workflow gained a gitleaks scan of the source tree, and safegit's own scrub and scan tests plant synthetic credential strings in throwaway repositories -- which is exactly what makes them useful fixtures. With no allowlist the scan found five and failed both publish jobs, so the 0.25.0 GitHub Release carried no platform binaries and no image was pushed to the container registry. A
.gitleaks.tomlnow names those four fixture literals, so the scan passes without exempting the files a real credential could land in. The Go module was unaffected --go install github.com/smm-h/[email protected]has worked all along.
#0.25.0
safegit backup keeps a per-branch history snapshot on a remote, --dry-run is honest in every command that accepts it, only the four history-rewriting commands confirm (via --approve-consequential), and the rlsbl rewrite handshake is gone
Context
Three things happened here, and they are connected.
The first is safegit backup. The tool has always been careful about the working tree and the commit path, but the one thing it could not do was give you somewhere to put a branch before you did something frightening to it. The new group keeps exactly one slot per branch under refs/backups/<branch> on a remote: backup backup refuses to overwrite a slot holding commits your history does not contain, pins the push with a lease to the SHA it just observed so a concurrent backup from another machine is rejected rather than clobbered, and asks first when the remote is public or cannot be proven private. Every slot is plain-git restorable, which is the point -- a backup you need safegit to read is not much of a backup.
The second is that --dry-run now means something. It used to be a per-command courtesy: eleven commands accepted the flag and mutated anyway, so safegit --dry-run push really pushed, and a dry-run commit inside a submodule created a real commit in the parent repository. The mutating seams now route through the CLI framework's effects handle, so a dry run records what it would do and prints it instead of doing it. That is a guarantee rather than a habit, because there is now one place it could break instead of thirty.
The third is the confirmation story, and it is where the breaking changes are. Confirmation used to be inferred from "this command mutates something", which caught roughly two thirds of the commands in the tool -- commit, push, pull, undo among them -- and a prompt that fires on two thirds of invocations trains people to dismiss it, which is the opposite of what a confirmation is for. Consequence is now declared per command, and exactly four declare it: scrub file, scrub match, scrub run and author rewrite, the ones that rewrite history irreversibly. Everything else runs bare again, with no flag and no prompt. The flag that consents is --approve-consequential, deliberately unwieldy so it cannot decay into muscle memory the way -y did, and self-documenting wherever it appears in a script. --yes is gone, and so are -q, -n and -y: the reserved quartet is framework-owned and has no short forms, in exchange for being accepted anywhere on the command line. --json no longer answers a confirmation on your behalf, and a declined confirmation exits 1 instead of 0, so a script cannot read a refusal as success.
Also gone: the guard that killed scrub and author rewrite in any repository containing .rlsbl/ unless RLSBL_SCRUB_ORCHESTRATED=1 was exported. That was prevention by handshake -- two tools had to agree on an environment variable before either could do its job, and the failure mode was a hard stop on a legitimate rewrite. Every rewrite already writes a journal at .git/safegit/rewrite-maps.jsonl; the release tooling reads that journal to detect the metadata a rewrite invalidated and repair it. Detecting and healing after the fact turned out to be both simpler and stricter than refusing beforehand.
#Breaking
- Destructive history rewrites are no longer blocked in release-managed repositories.
scrub file,scrub match,scrub runandauthor rewriteused to die in repos containing.rlsbl/unlessRLSBL_SCRUB_ORCHESTRATED=1was set; that handshake is gone and the rewrites run like anywhere else. Every rewrite still writes the journal at.git/safegit/rewrite-maps.jsonl, which the release tooling reads to detect and repair the metadata a rewrite invalidates. - **
--jsonno longer confirms destructive operations for you.** A--jsonrun used to imply consent, sosafegit --json scrub match ...,--json author rewriteand--json doctor --uninstallwent ahead without asking. They now refuse and name--approve-consequentialas the flag that consents deliberately;--jsonstill means machine-readable output. Previews are unaffected, andscrub file --dry-runno longer asks for permission to rewrite nothing. - **Breaking:
--quiet,--verbose,--dry-runand--approve-consequentialare framework-owned, and the short forms are gone.** Write them in full ---q,-nand-yno longer exist. In exchange all four are recognized anywhere on the command line, sosafegit push --dry-runworks as well assafegit --dry-run push. Every command is now classified read-only or mutating, which is what decides whether--dry-runrecords the command's effects instead of performing them; read-only commands (version,scan,config show/get,author list/check,backup list,hook list,scrub verify) have nothing to record. - Only history-rewriting commands ask for confirmation now.
safegit commit,push,pull,undo,config setand the guarded passthroughs run bare again -- no flag, no prompt. Confirmation is reserved for the four commands that rewrite history irreversibly (scrub file,scrub match,scrub run,author rewrite), and the flag that skips it is now--approve-consequential;--yesis gone.
#Features
- **
CLAUDE_CODE_SESSION_IDis now documented insafegit --help.** The session handshake variable that scopessafegit undoand stamps commit trailers is declared to the CLI framework and listed under Infrastructure. - **New
safegit backupcommand group.**backup backup,backup listandbackup restorekeep one backup slot per branch underrefs/backups/<branch>on a remote: the backup refuses to overwrite a slot containing work missing from your history, leases the push to the SHA it just observed, warns before pushing to a public remote, and every slot stays restorable with plain git. - Commands guide documents the backup group. The guide covers
backup backup/list/restore, the divergence refusal and lease, the public-remote confirmation, and the plain-git commands each subcommand is equivalent to. - Three new documentation guides, and a README and CLAUDE.md generated from templates. The docs site gains a commands guide covering every subcommand with its flags, exit codes and plain-git equivalent; a concurrency guide explaining the locking model and how concurrent sessions interact; and an integration guide for wiring safegit into other tools.
README.mdandCLAUDE.mdare now generated by selfdoc fromdocs/_README.mdanddocs/_CLAUDE.md, so they stay in step with the rest of the documentation.
#Fixes
- **
author rewrite --dry-runno longer contends for the rewrite lock.** The read-only preview previously loaded config and acquired the repo-wide rewrite lock, so it could block and fail while an unrelated scrub was running. - **
--dry-runin a submodule no longer commits in the parent repository.** Submodule auto-bump ran on the dry-run path too, so previewing a commit, amend or reword inside a submodule could create a real parent commit; the preview now leaves the parent untouched. - **
--jsonno longer bypasses the backup public-remote confirmation.**safegit --json backup backup <remote>used to push a full branch snapshot to a public or unclassifiable remote without asking, because--jsonimplied consent. It now refuses and names--approve-consequentialas the flag that consents deliberately. - **
backup backup --dry-runno longer contacts the remote.** The preview is built from local state, so it works against an unreachable remote and never asks the public-remote question. The slot's SHA, the ancestry check and the lease are resolved when the backup actually runs. - **
--dry-runis honest everywhere now.**push,pull,checkout,merge,rebase,reset,bisect,cherry-pick,revert,config setandhook installall ignored--dry-runand mutated anyway --safegit --dry-run pushreally pushed. Every one of them now records what it would do in a would-do log on stdout and changes nothing. - **
commit --dry-runno longer reports a commit it did not make.** The preview printed<n> file(s) committed; it now sayswould be committedand lists the ref update it would perform. - A declined confirmation now exits nonzero. Answering
n(or feeding EOF) todoctor --uninstall, a public-remotebackup backup, or any scrub/rewrite prompt printedAborted.and exited 0, so a script or agent could read a refusal as success. Every declined confirmation now exits 1.
#0.24.0
Breaking: --config renamed to --config-file (strictcli reserves the global name 'config'); go-strictcli 0.20.0.
#Breaking
- Breaking.
--configrenamed to--config-file(strictcli now reserves the global nameconfig).
#0.23.5
Fix Windows process-alive detection (locks and GC now work correctly on Windows).
#Fixes
- Windows process-alive check now uses OpenProcess API instead of broken Signal(0), fixing lock contention and premature tmp directory cleanup on Windows.
#0.23.4
Fix flaky TestCleanRepo: SharedSafegitDir resolves git-common-dir from the correct repository.
#Fixes
- Fix. SharedSafegitDir now resolves git-common-dir from the correct repository, fixing a race where parallel tests modified the host project's .git/safegit/ instead of the test repo's.
#0.23.3
Fix Windows cross-compilation in goreleaser builds.
#Fixes
- Fix. Windows cross-compilation now succeeds (extracted platform-specific syscalls into shared filelock, procutil, and hooks packages with build constraints).
#0.23.2
Fix CI workflow missing protocol.file.allow env
Context
The ci-go.yml workflow was missing GIT_CONFIG env vars that ci.yml already had, causing submodule tests to fail on that runner.
#Fixes
- Fix. Add missing protocol.file.allow environment to ci-go.yml workflow, completing the CI submodule test fix.
#0.23.1
Fix CI submodule test failures on GitHub Actions
#Fixes
- Fix. CI submodule tests no longer fail on GitHub Actions runners with restricted file transport protocol.
#0.23.0
Undo overhaul: --count N, redo removal, root commit fix
Context
The redo command is removed in favor of undo --count N, which provides a simpler mental model for multi-step undo. Root commit undo is fixed by introducing DeleteRef and ReadTreeEmpty to properly handle the branch-less state after undoing a first commit.
#Breaking
- Breaking: removed redo command. Redo is replaced by undo --count N. To recover from over-undo, re-commit instead of redo.
#Features
- New: undo --count N. Undo multiple commits in a single invocation by specifying a count. Walk-back logic replays N undo steps sequentially.
#Fixes
- Improved CLI help text. App and scrub group help text now includes numeric data points for documentation quality compliance.
- Fixed undo on root commits. Undoing the first commit in a repository no longer crashes; the branch ref is deleted and the index cleared to match git-init state.
#0.22.0
Release orchestration integration: crash-safe rewrite maps, rlsbl orchestration guard on all destructive rewrites, and in-history SHA remapping.
Context
This release makes safegit's history rewrites safe to drive from release tooling. Every scrub now persists crash-safe rewrite records to .git/safegit/rewrite-maps.jsonl before, during, and after refs move -- including the edge case where only a tag annotation is rewritten -- so an orchestrator (or a crash recovery) can always reconstruct what moved where. Destructive scrubs and author rewrites in rlsbl-managed repositories now refuse to run outside release orchestration, closing the gap where an agent could rewrite history behind the release pipeline's back. The new --remap-shas-in flag keeps hash-referencing files such as JSONL changelogs self-consistent at every commit of the rewritten history.
#Features
- Rewrite-map persistence. Every scrub now records crash-safe rewrite maps in
.git/safegit/rewrite-maps.jsonl(commit mappings written before refs move, all tag rewrites, pre-rewrite remote-tracking state, cleanup status), and scrub JSON output gainspre_rewrite_remotes,cleanup_ok, andcleanup_errorskeys for release orchestrators. - Orchestration guard. Destructive
scrub file/scrub match/scrub runoperations in rlsbl-managed repositories now fail with a hard error directing users torlsbl release scrub; dry-run and--diffpreviews remain available. - **
--remap-shas-in <glob>.** New repeatable flag onscrub file,scrub match, andscrub runthat rewrites full 40-character commit hashes inside glob-matched files to the post-scrub SHAs during the walk, keeping hash-referencing files like JSONL changelogs self-consistent at every commit; pre-range references are left untouched and stale hashes are reported non-fatally. - **Orchestration guard covers
author rewrite.** Destructive author rewrites in rlsbl-managed repositories now fail with a hard error directing users to coordinate the rewrite through the release tooling, matching the scrub guard;--dry-runpreviews remain available.
#Fixes
- Tag-annotation-only rewrites are now recorded. When a scrub's secret lived only in a tag annotation body (no commit changed), the tag ref moved without a rewrite-maps record and the old secret-bearing tag object survived cleanup, failing post-scrub verification. Such rewrites now persist the full start/refs/complete record sequence and prune the old tag object; pure no-op scrubs stay recordless.
#0.21.1
Internal cleanup: SEO, hook migration, DRY extraction, test modernization
Context
Infrastructure-only release. Expanded CLI help texts for selfdoc SEO compliance. Migrated pre-release hooks to config-driven format. Extracted duplicated commit count estimation into shared helper. Migrated tree_ops_test.go from os.Chdir to git.WithDir for test isolation.
#Infrastructure
- Internal cleanup: SEO, hook migration, DRY extraction, test modernization
#0.21.0
Revert tracked policy file — scrub patterns are no longer committed to the repo
Context
The v0.20.0 decision to track .safegit/scrub-policies.jsonl in the working tree was a security mistake: the policy file contains the literal regex patterns used to find secrets, which re-introduces the scrubbed content into the repo. Reverted to .git/safegit/ (untracked). Deleted ~250 lines of complexity: auto-commit logic, policy blob exclusion sets, migration from old to new location. Policies are now local to the machine where the scrub was performed.
#Breaking
- Breaking. Scrub policy storage reverted from tracked
.safegit/to untracked.git/safegit/. Tracked policies committed the literal patterns being scrubbed, re-introducing secrets into the repo. Policies are now local-only and never committed.
#Features
- Improvement.
scrub verifyprints the policy file path and explains that policies are local to the machine when no policies are found.
#0.20.2
Fix per-operation recipe scope enforcement and dry-run lock acquisition
Context
Per-operation scope fields in recipe TOML files were recorded in policies but not enforced during blob filtering. scrub match and scrub file dry-run modes unnecessarily acquired the exclusive rewrite lock, blocking concurrent operations during read-only preview.
#Fixes
- Bug fix. Per-operation
scopein recipe TOML files is now enforced during execution and dry-run. Previously, recipe scopes were recorded in policies but not used for blob filtering. - Bug fix.
scrub match --dry-runno longer acquires the exclusive rewrite lock, allowing concurrent operations during preview. - Bug fix.
scrub file --dry-runno longer acquires the exclusive rewrite lock.
#0.20.1
Fix dry-run and preview modes that were writing objects to the git store
Context
Three dry-run/preview code paths were writing real objects to the git object store, contradicting their documented read-only behavior. scrub run --diff wrote replacement blobs via HashObjectWriteBytes. scrub file --dry-run wrote the replacement blob before the dry-run gate. Both are now truly read-only. Additionally, scrub run --dry-run is a new mode showing per-operation match counts without any writes or lock acquisition.
#Features
- New feature.
scrub run --dry-runshows per-operation match counts and affected files without writing objects or acquiring the rewrite lock.
#Fixes
- Bug fix.
scrub run --diffno longer writes blob objects to the git store. Preview is now truly read-only. - Bug fix.
scrub file --dry-runno longer writes the replacement blob to the git store. SHA is computed without persisting.
#0.20.0
Scan DRY consolidation, unified scrub execution, os.Chdir elimination, tracked policy files
Context
Internal architecture improvements and one breaking change.
The scan package was consolidated from 6 nearly-identical functions to a unified ScanOpts-based API. scrub match now delegates to the shared recipe execution engine (executeScrubRecipe), eliminating ~300 lines of duplicated orchestration. scrub verify scans the object store once for all policies instead of once per policy.
All 24 os.Chdir calls in submodule processing were replaced with context-scoped git directories (git.WithDir). Git commands now target the correct repository via context values, not process-wide CWD mutation.
Scrub policy files moved from .git/safegit/ (hidden, per-worktree) to .safegit/ (tracked, committed, portable across clones). Existing repos auto-migrate on first read. This is a breaking change for tooling that reads the old location directly.
strictcli upgraded from v0.9.0 to v0.16.2. JSON-producing commands are tagged in the CLI schema.
#Breaking
- Breaking. Scrub policy file moved from
.git/safegit/scrub-policies.jsonlto.safegit/scrub-policies.jsonl(tracked in the working tree). Policies are now committed, portable across clones, and visible in PRs. Existing repos auto-migrate on first read.
#Features
- Improvement. JSON-producing commands are now tagged in the CLI schema, making it machine-discoverable which commands support
--json. - Internal. Consolidated the scan package from 6 functions to a unified
ScanObjects(ctx, pattern, opts)API withScanOptsstruct. - Performance.
safegit scrub verifynow scans the object store once for all policies instead of once per policy. - Internal.
scrub matchnow delegates execution to the shared recipe engine, eliminating ~300 lines of duplicated orchestration code. - Internal. Replaced all 24
os.Chdircalls in submodule processing with context-scoped git directories. Git commands now target the correct repository via context, not process-wide CWD mutation.
#0.19.0
New scan, author, and recipe commands; scrub verification; breaking rename of rewrite-author
Context
This release adds five new commands and restructures the CLI around an author group.
safegit scan is a top-level read-only search across git history with --target filtering (blobs, commits, tags, trailers, files). The author group provides safegit author list (identity auditing), safegit author check (deviation detection), and safegit author rewrite (renamed from the top-level rewrite-author, which now prints a deprecation error).
safegit scrub run executes multi-operation scrub recipes from TOML files with independent and chained operations, per-operation scope/target filters, and --diff preview. safegit scrub verify reads a persistent policy file (auto-populated after scrubs) to continuously verify that removed secrets stay gone.
Internally, all three history-rewriting commands now share a RewriteResult pipeline (Finalize method) that handles ref updates, index sync, cleanup, oplog, and push hints in one place. rewrite-author gains previously-missing index sync and object cleanup. Push hints now detect .rlsbl-managed repos. Tree caching reduces git plumbing calls during scrubs. Multi-line regex patterns now work correctly in scan/dry-run mode.
#Breaking
- Renamed.
safegit rewrite-authoris nowsafegit author rewrite. The old name prints a deprecation error.
#Features
- New command.
safegit scansearches git history for regex patterns across blobs, commit messages, tag annotations, trailers, and working tree files. Supports--targetfiltering,--scopeglob,--from/--entire-historyrange, and--jsonoutput. - New commands.
safegit author listenumerates all author/committer identities with commit counts.safegit author checkcompares identities against expected values and suggests rewrite commands for mismatches. - New command.
safegit scrub runexecutes multi-operation scrub recipes from TOML files. Supports independent and chained operations (depends_on), per-operationscopeandtargetfilters,--diffpreview mode, and overlap detection. - New command.
safegit scrub verifychecks that previously scrubbed patterns remain absent from git history. Reads from a persistent policy file auto-populated after scrub operations. Supports hand-written policies for preventive scanning. - Unified post-rewrite pipeline. All history-rewriting commands (
scrub file,scrub match,author rewrite) now share aRewriteResult.Finalizemethod that handles index sync, working tree update, object pruning, oplog, verification, and push hints. This replaces per-command post-rewrite logic with a single consistent pipeline. - Performance. Scrub operations now cache tree objects during the rewrite walk, avoiding redundant
git ls-treeandgit mktreecalls for unchanged subtrees.
#Fixes
- Bug fix.
author rewritenow updates identity-bearing trailers (Co-authored-by,Signed-off-by,Reviewed-by,Acked-by) alongside commit header fields. - Bug fix.
author rewritenow syncs the working tree index and prunes old objects after rewriting, matching the behavior of scrub commands. Previously the working tree could be stale and old objects would linger. - Bug fix. Pattern scanning now matches on full blob content instead of line-by-line, fixing multi-line regex patterns that previously produced zero matches in dry-run mode.
#0.18.4
Fix commit --amend on root commits.
Context
amend.go rejected root commits (no parent) even though reword and CommitTree already supported them. One-line fix matching the existing reword pattern.
#Fixes
- **
commit --amendnow works on root commits.** Previously failed with 'is a root commit (no parent)'. The fix matches the existing pattern used byreword.
#0.18.3
Fix rewrite-author --quiet, add ScopeFilter to dry-run JSON, submodule-aware range scanning infrastructure.
Context
Loose ends from v0.18.2: rewrite-author summary had unconverted fmt.Printf calls, dry-run JSON lost the --scope glob value, and ScanObjectsInRangeWithDir was added for future submodule range-scoping.
#Features
- Submodule-aware range-scoped scanning. Added
ScanObjectsInRangeWithDirandCatFileBatchSHAsWithDirfor future submodule range-scoped dry-run support.
#Fixes
- **
rewrite-authorrespects--quiet.** Summary output is suppressed when--quietis passed.
#0.18.2
Add rewrite-author --json, range-scoped dry-run scanning, JSON error safety net, splitNonEmpty consolidation.
Context
Range-scoped dry-run uses git rev-list --objects to scan only range-reachable objects instead of the entire store. rewrite-author now supports --json for both execute and dry-run modes. die() emits JSON error objects when --json is active, and mustGitDir routes through die() for coverage. splitNonEmpty consolidated from two implementations into a single exported git.SplitNonEmpty.
#Features
- Scrub match dry-run scans only range-reachable objects. When
--fromis specified, the dry-run now scans only objects reachable from the commit range instead of the entire object store. Faster and more accurate for scoped rewrites. - **
rewrite-author --jsonemits machine-readable output.** JSON includes old-to-new commit SHA mapping, tag rewrites, counters, and author details. Dry-run mode also emits JSON.
#Fixes
- **Consolidated
splitNonEmptyinto a single exportedgit.SplitNonEmptyfunction.** Eliminates duplication between the main package and internal/git versions. - **
--jsonerrors now emit structured JSON.** When--jsonis active and a fatal error occurs,die()emits{"error": "..."}to stdout before exiting. Also fixesmustGitDirto route throughdie()so the JSON safety net covers early exits.
#0.18.1
Fix push test CI failure on ubuntu-latest.
Context
Bare remotes in tests used system default branch (master on CI) while local repos used main. Fixed by setting --initial-branch=main on all bare inits.
#Fixes
- Fixed CI test failure for push commands. Tests now use explicit branch names for bare remote verification, fixing failures on systems where the default branch is not
main.
#0.18.0
Add global --json flag, scrub JSON output, explicit push mode flags.
Context
Push now requires an explicit mode flag (--only-head, --only-branches, --only-tags, --both-branches-and-tags) instead of implicit current-branch default and refspec positional args. Scrub match and scrub file support --json for machine-readable output (commit SHA mapping, tag rewrites). This unblocks rlsbl release scrub (upstream consumer). Scrub commands now respect --quiet. All post-rewrite suggestions route through safegit push.
#Breaking
- **Push now requires explicit mode:
--only-head,--only-branches,--only-tags, or--both-branches-and-tags.** Replaces the implicit current-branch default and refspec positional args. Each mode enumerates refs explicitly rather than passing--all/--tagsto git.
#Features
- **Scrub commands respect
--quiet.** All informational output is suppressed when--quietis passed. - **
scrub match --jsonemits machine-readable output.** JSON includes old-to-new commit SHA mapping, tag rewrites, and counters. Dry-run mode emits scan results. Blockedrlsbl release scrub(upstream consumer). - **
scrub file --jsonemits machine-readable output.** Same JSON schema as scrub match, including dry-run mode.
#0.17.2
Sync submodule working trees after scrub history rewrites.
Context
v0.17.1 added working-tree sync for the parent repo but missed submodule child repos. Submodule files on disk could still contain pre-scrub secrets.
#Fixes
- Scrub now syncs submodule working trees after rewriting history. Previously only the parent repo's working tree was updated; submodule files on disk could still contain pre-scrub content.
#0.17.1
Fix scrub data loss for tracked+gitignored files, fix doctor dry-run submodule reporting, add unlock --dry-run.
Context
The v0.16.0 working-tree sync (read-tree --reset -u) could overwrite tracked+gitignored files (e.g., config files with runtime secrets that were committed by mistake then gitignored). SyncMainIndexWithWorktree now saves and restores their content, then untracks them. Also fixed doctor --fix --dry-run not reporting submodule state, and added --dry-run support to unlock.
#Features
- **
unlocknow supports--dry-run.** Shows what would be released without actually removing the lock.
#Fixes
- **
doctor --fix --dry-runnow reports submodule state.** Previously the dry-run path returned early before the submodule loop, so submodule stale locks and orphan tmp dirs were never reported in dry-run mode. - Scrub preserves tracked+gitignored files. When a committed-then-gitignored file is scrubbed, the on-disk content is preserved and the file is automatically untracked. Prevents data loss for config files with runtime secrets.
#0.17.0
Rename --force-push to --force-with-lease, remove --push from rewrite-author, fix doctor stale lock cleanup.
Context
Loose ends from the v0.16.0 --force removal: the --force-push flag name was misleading (it triggers --force-with-lease), rewrite-author --push bypassed safegit's push pipeline, and doctor --fix didn't clean stale locks in the main repo. Also added integration tests for unlock (previously untested) and doctor stale lock cleanup.
#Breaking
- **Renamed
--force-pushto--force-with-leaseonpush.** The flag name now matches what it actually does. - **Removed
--pushflag fromrewrite-author.** After rewriting, safegit now prints the git commands to push manually instead of pushing directly.
#Fixes
- Removed stash suggestion from dirty-tree error.
requireCleanTreeno longer suggestsgit stashas a workaround. - **
doctor --fixnow cleans stale locks in the main repo.** Previously only cleaned submodule stale locks. The diagnostic check also now scans the full lock tree recursively instead of justrefs/heads/.
#0.16.1
Fix macOS symlink bug in submodule detection.
Context
DetectParent compared symlink-resolved paths from git with unresolved paths from os.Getwd(), causing filepath.Rel to produce incorrect relative paths on macOS where /var -> /private/var. This broke autobump and push-hook-cascade on macOS since v0.15.0.
#Fixes
- Fixed macOS symlink bug in submodule detection.
DetectParentnow resolves symlinks on bothparentWorkTreeandcwd, fixing autobump and push-hook-cascade on macOS where/varis a symlink to/private/var.
#0.16.0
Remove global --force flag, add --yes and --mangle, sync working tree after scrub, use --force-with-lease for pushes.
Context
The global --force flag was overloaded across 10+ use sites with 5+ distinct meanings. This release replaces it with explicit per-purpose mechanisms: --yes for confirmation prompts, unconditional dirty-tree rejection, no coordination guard bypass, no hook skip, no gitignore override. Push now uses --force-with-lease (safe force push) instead of --force (destructive). Scrub operations now sync the working tree after rewriting history, so secrets no longer linger on disk. New --mangle flag for scrub match replaces matched content with crypto-random printable ASCII of the same length.
#Breaking
- **Removed global
--forceflag.** Replaced with explicit per-purpose mechanisms:--yesfor confirmation prompts, unconditional dirty-tree rejection, no coordination guard bypass, no hook skip, no gitignore override. - **Push uses
--force-with-lease.**safegit push --force-pushandrewrite-author --pushnow use--force-with-leaseinstead of--force, preventing accidental overwrites of others' pushes.
#Features
- Working tree sync after scrub.
scrub matchandscrub filenow update working tree files to match rewritten history, so secrets no longer linger on disk after scrubbing. - **New
--mangleflag forscrub match.** Replaces matched content with crypto-random printable ASCII of the same length, as a mutually exclusive alternative to--replace. - **New
--yes/-yglobal flag.** Auto-confirms interactive prompts for scrub, rewrite-author, and doctor commands.
#0.15.3
Fix macOS symlink path resolution in submodule discovery.
#Fixes
- Fix: submodule path resolution now handles macOS
/tmpsymlinks correctly.
#0.15.2
Fix macOS CI: use portable GIT_CONFIG env vars instead of --global for file transport allow.
#Fixes
- Fix: submodule tests now pass on macOS CI (portable git config via environment variables).
#0.15.1
Fix submodule test infrastructure for CI environments with restricted file transport.
#Fixes
- Fix: submodule integration tests now pass on CI environments that restrict
protocol.file.allow.
#0.15.0
Full submodule support: commit/undo/redo inside submodules, auto-bump parent pointer, scrub auto-recurse, push hook cascade, doctor cleanup.
Context
safegit previously refused to operate in repos with submodules. This release removes that limitation and adds deep submodule integration across all commands.
#Features
- Submodule support. safegit now works in repos with submodules. Commit, undo, redo, and amend all operate correctly when cwd is inside a submodule, with lock isolation between parent and submodule repos.
- Auto-bump parent pointer. After committing in a submodule, safegit automatically commits the pointer update in the parent repo. Controlled by
commit.autoBumpParentconfig (mandatory boolean). Triggers on commit, amend, reword, undo, and redo. - Scrub auto-recurse into submodules.
scrub matchandscrub fileautomatically scan and rewrite secrets inside submodules, updating parent gitlink pointers. Supports--scopefiltering by submodule path. - Push hook cascade. When pushing from a submodule, parent repo's pre-pre-push hooks are discovered and run first.
- Doctor cleans submodule state.
doctor --fixnow finds and cleans orphan tmp dirs and stale locks in all submodule safegit directories.
#Fixes
- Fix: committing a submodule pointer update (
safegit commit -- mysub) no longer fails due to move detection trying to hash a directory.
#0.14.2
Improved documentation descriptions for AI discoverability.
#Fixes
- Improved documentation descriptions. Expanded CLI help strings and package doc comments for better AI discoverability.
#0.14.1
Rewrite-author lock, scrub match --scope flag, CLAUDE.md update.
#Features
- New flag: scrub match --scope. Glob pattern limiting which file paths are searched (e.g.
--scope '*.env'). Blobs at paths outside scope are left untouched.
#Fixes
- Rewrite-author now uses the repo-wide rewrite lock. Prevents concurrent rewrite-author and scrub operations from corrupting history.
#0.14.0
New scrub match command for pattern-based secret removal with surgical cleanup.
Context
Major release driven by a real secret leak incident. A user ran scrub on leaked API keys and found secrets persisted in reflog entries and unreachable objects.
New: safegit scrub match --pattern
Breaking: safegit scrub is now a command group. The old 'safegit scrub
Also: shared walkAndRewrite extraction, ParseCommit dedup in verification, branch ref verification, streaming object enumeration, new scan package.
#Breaking
- Breaking: scrub is now a command group.
safegit scrubbecomessafegit scrub file(existing behavior) andsafegit scrub match(new). The oldsafegit scrub <file> --from <commit>syntax no longer works. - Breaking: TreeEntry.BlobSHA renamed to TreeEntry.SHA. Reflects that the field stores both blob and tree SHAs. All callers updated.
#Features
- New command: scrub match. Pattern-based secret removal across all git objects — blobs, commit messages, and tag annotations. Use
--dry-runto scan without rewriting. - Surgical post-rewrite cleanup. Scrub now expires tainted reflog entries, prunes unreachable objects, and re-scans to verify secrets are gone. Hard-errors if any matches survive.
- Refactor: extracted shared commit walker.
walkAndRewriteeliminates code duplication between scrub file, rewrite-author, and scrub match. - New git plumbing. Streaming object enumeration (
CatFileBatchAll), in-memory blob writing (HashObjectWriteBytes), blob reading (CatFileBlob), and SHA-identity tree replacement (replaceInTreeByBlobMap).
#Fixes
- Scrub verification: deduplicated ParseCommit calls. 4x fewer git subprocess calls during post-rewrite verification.
- Scrub verification: branch ref checks. New check 7 verifies branch refs were correctly remapped after rewrite.
#0.13.0
Breaking: --from is now inclusive. Scrub safety improvements.
Context
Three scrub improvements:
- --from is now inclusive: the commit you point to IS rewritten, not just everything after it. An ancestry guard rejects non-ancestral --from commits.
- Dirty-tree guard prevents scrub from clobbering staged changes (use --force to override). SyncMainIndex after rewriting keeps git status clean.
- Post-rewrite verification now detects tags that still point to pre-rewrite commits.
#Breaking
- Breaking: --from is now inclusive.
safegit scrub --from Xnow includes commit X in the rewrite. Previously it started after X (exclusive). An ancestry guard now rejects --from commits not ancestral to HEAD.
#Fixes
- Scrub safety: dirty-tree guard and index sync. Scrub now checks for a clean working tree before starting (use --force to override) and syncs the main index after rewriting, preventing phantom
git statusoutput. - Scrub verification: stale tag detection. Post-rewrite verification now catches tags that still point to pre-rewrite commits, detecting
updateRefsfailures.
#0.12.0
Session-scoped undo, redo command, and scrub command for surgical history rewriting.
Context
Three new features aimed at multi-session safety and sensitive content cleanup:
- safegit undo is now session-scoped by default (requires CLAUDE_CODE_SESSION_ID), preventing accidental cross-session rollbacks. --bypass-session restores old behavior.
- safegit redo explicitly restores what undo removed, with one-shot design to prevent oscillation.
- safegit scrub
--from --reason surgically replaces or removes a file's blob across history, with post-rewrite verification, annotated tag rewriting, and confirmation prompt.
#Features
- New command: scrub.
safegit scrub <file> --from <commit> --reason <text>surgically replaces or removes a file's blob across all commits from a starting point to HEAD, with post-rewrite verification and annotated tag rewriting. - New command: redo.
safegit redorestores whatsafegit undoremoved. One-shot by design -- prevents infinite undo/redo oscillation. - Session-scoped undo.
safegit undonow only undoes the current session's operations by default, preventing accidental cross-session rollbacks. Use--bypass-sessionfor the old behavior.
#0.11.1
#Fixes
- Fix. CI matrix updated to Go 1.25+1.26 to match go.mod requirement.
- Fix. Dockerfile updated to Go 1.25 to match go.mod requirement.
#0.11.0
#Features
- New feature. Auto-detect file moves on commit. When committing a file that was moved (via
mv), safegit now automatically stages the deletion of the old path, so the commit records a proper rename. Works with bothcommitandcommit --amend. Uses exact content matching (move+edit requires listing both paths explicitly).
#0.10.3
#Fixes
- Fix: docs deploy in post-release hook. Environment variables are now exported so selfdoc/wrangler can read Cloudflare credentials.
#0.10.2
- No user-facing changes.
#0.10.1
#Fixes
- Fix: use published strictcli dependency. Removed local replace directive so
go installand CI work correctly.
#0.10.0
#Breaking
- Breaking: CLI restructured with strictcli. Commands now use structured parsing with auto-generated help.
configandhookare subcommand groups (config set key value,hook list).pullrequires--ff-only,--ff, or--no-ff.doctorrequires--diagnose,--fix, or--uninstall.push --forcerenamed to--force-push.
#Features
- **New:
--trailerflag.**safegit commit --trailer "Key: Value"appends git trailers to commit messages. Repeatable, works with commit, amend, and reword.
#Fixes
- **Fix:
--allow-emptywithout files.** Previously rejected with "no files specified" even when--allow-emptywas set.