safegit v0.26.0 /Changelog
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-consequential no longer consents to backing up to a public remote; --allow-public-remote does.** 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-consequential had 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 backup gains --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 --uninstall is unchanged.

#Fixes

  • **safegit --dry-run hook run no longer runs the hooks.** The command ignored --dry-run outright, 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 -- so hook run now declares that it has no dry-run mode and the flag is refused with that reason. safegit hook list shows 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 -- unlock takes a ref and no flags -- so anyone following it got a parse error. It now describes what unlock does, 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, and hook run gained 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.toml now 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 run and author rewrite used to die in repos containing .rlsbl/ unless RLSBL_SCRUB_ORCHESTRATED=1 was 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.
  • **--json no longer confirms destructive operations for you.** A --json run used to imply consent, so safegit --json scrub match ..., --json author rewrite and --json doctor --uninstall went ahead without asking. They now refuse and name --approve-consequential as the flag that consents deliberately; --json still means machine-readable output. Previews are unaffected, and scrub file --dry-run no longer asks for permission to rewrite nothing.
  • **Breaking: --quiet, --verbose, --dry-run and --approve-consequential are framework-owned, and the short forms are gone.** Write them in full -- -q, -n and -y no longer exist. In exchange all four are recognized anywhere on the command line, so safegit push --dry-run works as well as safegit --dry-run push. Every command is now classified read-only or mutating, which is what decides whether --dry-run records 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 set and 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; --yes is gone.

#Features

  • **CLAUDE_CODE_SESSION_ID is now documented in safegit --help.** The session handshake variable that scopes safegit undo and stamps commit trailers is declared to the CLI framework and listed under Infrastructure.
  • **New safegit backup command group.** backup backup, backup list and backup restore keep one backup slot per branch under refs/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.md and CLAUDE.md are now generated by selfdoc from docs/_README.md and docs/_CLAUDE.md, so they stay in step with the rest of the documentation.

#Fixes

  • **author rewrite --dry-run no 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-run in 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.
  • **--json no 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 --json implied consent. It now refuses and names --approve-consequential as the flag that consents deliberately.
  • **backup backup --dry-run no 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-run is honest everywhere now.** push, pull, checkout, merge, rebase, reset, bisect, cherry-pick, revert, config set and hook install all ignored --dry-run and mutated anyway -- safegit --dry-run push really pushed. Every one of them now records what it would do in a would-do log on stdout and changes nothing.
  • **commit --dry-run no longer reports a commit it did not make.** The preview printed <n> file(s) committed; it now says would be committed and lists the ref update it would perform.
  • A declined confirmation now exits nonzero. Answering n (or feeding EOF) to doctor --uninstall, a public-remote backup backup, or any scrub/rewrite prompt printed Aborted. 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. --config renamed to --config-file (strictcli now reserves the global name config).

#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 gains pre_rewrite_remotes, cleanup_ok, and cleanup_errors keys for release orchestrators.
  • Orchestration guard. Destructive scrub file/scrub match/scrub run operations in rlsbl-managed repositories now fail with a hard error directing users to rlsbl release scrub; dry-run and --diff previews remain available.
  • **--remap-shas-in <glob>.** New repeatable flag on scrub file, scrub match, and scrub run that 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-run previews 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 verify prints 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 scope in 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-run no longer acquires the exclusive rewrite lock, allowing concurrent operations during preview.
  • Bug fix. scrub file --dry-run no 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-run shows per-operation match counts and affected files without writing objects or acquiring the rewrite lock.

#Fixes

  • Bug fix. scrub run --diff no longer writes blob objects to the git store. Preview is now truly read-only.
  • Bug fix. scrub file --dry-run no 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.jsonl to .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 with ScanOpts struct.
  • Performance. safegit scrub verify now scans the object store once for all policies instead of once per policy.
  • Internal. scrub match now delegates execution to the shared recipe engine, eliminating ~300 lines of duplicated orchestration code.
  • Internal. Replaced all 24 os.Chdir calls 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-author is now safegit author rewrite. The old name prints a deprecation error.

#Features

  • New command. safegit scan searches git history for regex patterns across blobs, commit messages, tag annotations, trailers, and working tree files. Supports --target filtering, --scope glob, --from/--entire-history range, and --json output.
  • New commands. safegit author list enumerates all author/committer identities with commit counts. safegit author check compares identities against expected values and suggests rewrite commands for mismatches.
  • New command. safegit scrub run executes multi-operation scrub recipes from TOML files. Supports independent and chained operations (depends_on), per-operation scope and target filters, --diff preview mode, and overlap detection.
  • New command. safegit scrub verify checks 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 a RewriteResult.Finalize method 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-tree and git mktree calls for unchanged subtrees.

#Fixes

  • Bug fix. author rewrite now updates identity-bearing trailers (Co-authored-by, Signed-off-by, Reviewed-by, Acked-by) alongside commit header fields.
  • Bug fix. author rewrite now 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 --amend now works on root commits.** Previously failed with 'is a root commit (no parent)'. The fix matches the existing pattern used by reword.

#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 ScanObjectsInRangeWithDir and CatFileBatchSHAsWithDir for future submodule range-scoped dry-run support.

#Fixes

  • **rewrite-author respects --quiet.** Summary output is suppressed when --quiet is 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 --from is 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 --json emits 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 splitNonEmpty into a single exported git.SplitNonEmpty function.** Eliminates duplication between the main package and internal/git versions.
  • **--json errors now emit structured JSON.** When --json is active and a fatal error occurs, die() emits {"error": "..."} to stdout before exiting. Also fixes mustGitDir to route through die() 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/--tags to git.

#Features

  • **Scrub commands respect --quiet.** All informational output is suppressed when --quiet is passed.
  • **scrub match --json emits machine-readable output.** JSON includes old-to-new commit SHA mapping, tag rewrites, and counters. Dry-run mode emits scan results. Blocked rlsbl release scrub (upstream consumer).
  • **scrub file --json emits 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

  • **unlock now supports --dry-run.** Shows what would be released without actually removing the lock.

#Fixes

  • **doctor --fix --dry-run now 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-push to --force-with-lease on push.** The flag name now matches what it actually does.
  • **Removed --push flag from rewrite-author.** After rewriting, safegit now prints the git commands to push manually instead of pushing directly.

#Fixes

  • Removed stash suggestion from dirty-tree error. requireCleanTree no longer suggests git stash as a workaround.
  • **doctor --fix now 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 just refs/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. DetectParent now resolves symlinks on both parentWorkTree and cwd, fixing autobump and push-hook-cascade on macOS where /var is 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 --force flag.** Replaced with explicit per-purpose mechanisms: --yes for confirmation prompts, unconditional dirty-tree rejection, no coordination guard bypass, no hook skip, no gitignore override.
  • **Push uses --force-with-lease.** safegit push --force-push and rewrite-author --push now use --force-with-lease instead of --force, preventing accidental overwrites of others' pushes.

#Features

  • Working tree sync after scrub. scrub match and scrub file now update working tree files to match rewritten history, so secrets no longer linger on disk after scrubbing.
  • **New --mangle flag for scrub match.** Replaces matched content with crypto-random printable ASCII of the same length, as a mutually exclusive alternative to --replace.
  • **New --yes/-y global 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 /tmp symlinks 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.autoBumpParent config (mandatory boolean). Triggers on commit, amend, reword, undo, and redo.
  • Scrub auto-recurse into submodules. scrub match and scrub file automatically scan and rewrite secrets inside submodules, updating parent gitlink pointers. Supports --scope filtering 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 --fix now 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 --replace --reason --entire-history searches all git objects (blobs, commit messages, tag annotations) for a pattern and replaces matches. Includes surgical post-rewrite cleanup (tainted reflog entries expired, unreachable objects pruned) and re-scan verification that hard-errors if any matches survive.

Breaking: safegit scrub is now a command group. The old 'safegit scrub ' becomes 'safegit scrub file '. TreeEntry.BlobSHA renamed to TreeEntry.SHA.

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 scrub becomes safegit scrub file (existing behavior) and safegit scrub match (new). The old safegit 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-run to 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. walkAndRewrite eliminates 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 X now 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 status output.
  • Scrub verification: stale tag detection. Post-rewrite verification now catches tags that still point to pre-rewrite commits, detecting updateRefs failures.

#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 redo restores what safegit undo removed. One-shot by design -- prevents infinite undo/redo oscillation.
  • Session-scoped undo. safegit undo now only undoes the current session's operations by default, preventing accidental cross-session rollbacks. Use --bypass-session for 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 both commit and commit --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 install and CI work correctly.

#0.10.0

#Breaking

  • Breaking: CLI restructured with strictcli. Commands now use structured parsing with auto-generated help. config and hook are subcommand groups (config set key value, hook list). pull requires --ff-only, --ff, or --no-ff. doctor requires --diagnose, --fix, or --uninstall. push --force renamed to --force-push.

#Features

  • **New: --trailer flag.** safegit commit --trailer "Key: Value" appends git trailers to commit messages. Repeatable, works with commit, amend, and reword.

#Fixes

  • **Fix: --allow-empty without files.** Previously rejected with "no files specified" even when --allow-empty was set.
Search