Scrubbed one-way replication from private Git repos to public targets.
git-copy is a CLI tool that safely synchronizes Git repositories from private sources to public targets (GitHub, GitLab, Gitea, etc.) while automatically scrubbing sensitive information. It rewrites Git history to replace private usernames, exclude sensitive files, and apply custom text replacements.
- Automatic Scrubbing: Replaces private usernames and sensitive strings throughout Git history
- File Exclusion: Exclude files by pattern (e.g.,
.env,secrets/**, etc.) - Opt-In Override: Selectively include files that would otherwise be excluded
- History Replacement: Replace file contents throughout history (e.g., retroactively change LICENSE)
- Author Rewriting: Replace commit author information with public identities
- Empty Commit Pruning: Automatically drops commits that become empty after filtering
- Multi-Target: Sync to multiple destinations (GitHub, GitLab, Gitea)
- Multi-Account Support: Automatically uses correct credentials for different GitHub accounts
- Auto-Sync Daemon: Background service auto-discovers and syncs repos
- Topics/Tags: Copy repository topics from source to target
- Safe by Default: Validates scrubbed repos before pushing (blocks
.env,CLAUDE.md, etc.) - Efficient: Uses
git fast-export/fast-importfor fast history rewriting
go install github.com/obinnaokechukwu/git-copy/cmd/git-copy@latestOr build from source:
git clone https://github.com/obinnaokechukwu/git-copy
cd git-copy
go build -o git-copy ./cmd/git-copycd /path/to/your/private/repo
git-copy initThis creates a .git-copy/config.json file with your scrubbing rules. The private_username is automatically detected from your origin remote URL (e.g., github.com/your-username/repo → your-username).
git-copy add-targetFollow the interactive prompts to configure:
- Target label (e.g., "github-public")
- Provider (github, gitlab, gitea)
- Account/organization name
- Repository name
- Authentication credentials
git-copy syncThis will:
- Export your Git history
- Apply scrubbing rules (replace usernames, exclude files)
- Validate the scrubbed repo
- Push to the configured target(s)
The .git-copy/config.json file controls scrubbing behavior:
{
"private_username": "myPrivateUsername",
"defaults": {
"exclude": [
".env",
"secrets/**",
"*.key"
],
"opt_in": [],
"replace_history_with_current": [
"LICENSE",
"README.md"
],
"extra_replacements": {
"company-internal.example.com": "public.example.com"
}
},
"targets": [
{
"label": "github-public",
"provider": "github",
"account": "my-public-account",
"repo_name": "my-public-repo",
"replacement": "PublicName",
"public_author_name": "Public Name",
"public_author_email": "public@example.com"
}
]
}private_username: Your private username to be replaced in all text/commitsdefaults.exclude: File patterns to exclude (glob syntax,**supported)defaults.opt_in: Override exclusions for specific filesdefaults.replace_history_with_current: Files to replace with current content throughout history (see below)defaults.extra_replacements: Additional string replacements (old → new)targets[].label: Unique identifier for this sync targettargets[].provider:github,gitlab, orgiteatargets[].account: Target account/organizationtargets[].repo_name: Target repository nametargets[].replacement: String to replaceprivate_usernamewithtargets[].public_author_name: Name for rewritten commitstargets[].public_author_email: Email for rewritten commitstargets[].replace_history_with_current: Target-specific files to replace (merged with defaults)
The replace_history_with_current feature allows you to retroactively replace file contents throughout your entire Git history. This is useful when you need to make a file appear as if it was always a certain way.
Common use cases:
- Changing LICENSE from MIT to Apache 2.0 retroactively
- Updating README to reflect current branding from the start
- Fixing configuration files that contained wrong values historically
How it works:
Source history: Public history (after sync):
───────────────── ────────────────────────────
commit A: add files commit A: add files
commit B: add LICENSE (MIT) commit B: add LICENSE (Apache) ← replaced
commit C: fix bug commit C: fix bug
commit D: update LICENSE [DROPPED - became empty]
commit E: new feature commit E: new feature
- The current (HEAD) content of specified files is used
- When the file first appears in history, it gets the current content instead
- All subsequent commits that only modify these files are automatically dropped (they become empty)
- Commits that modify these files and other files keep the other changes
Example:
{
"defaults": {
"replace_history_with_current": ["LICENSE", "NOTICE"]
}
}This makes LICENSE and NOTICE appear unchanged throughout history, using their current content from the first commit where they appear.
Important notes:
- Files are replaced at their first occurrence in history, not injected into commits that never had them
- The file content is scrubbed (private username replacement still applies)
- Empty commits are pruned automatically - no trace of intermediate changes
If you have multiple GitHub accounts authenticated with gh auth login, git-copy automatically uses the correct credentials for each target:
# Check your authenticated accounts
gh auth status
# git-copy will use the right token based on target account
git-copy sync # Uses obinnaokechukwu's token for obinnaokechukwu/repoThis works for both repo creation and pushing. No manual token switching needed.
# Initialize git-copy in current repo
git-copy init [--repo PATH]
# Add a new sync target interactively
git-copy add-target [--repo PATH]
# Remove a sync target
git-copy remove-target <label> [--repo PATH]
# List configured targets
git-copy list-targets [--repo PATH]
# Sync to all targets (or specific target). Audits the scrubbed output by default.
git-copy sync [--repo PATH] [--target LABEL] [--audit] [--audit-remote]
# Disable post-sync audit (faster, less safe)
git-copy sync --audit=false
# Audit without syncing (local cache and/or remote mirror)
git-copy audit [--repo PATH] --target LABEL [--remote] [--string S ...]
# Show sync status
git-copy status [--repo PATH]The daemon automatically discovers and syncs git-copy enabled repositories:
# Start the daemon manually
git-copy serve
# Install daemon to run at system startup (Linux/macOS)
git-copy install
# Uninstall daemon service
git-copy install --uninstall
# Check daemon status (Linux)
systemctl --user status git-copy
# View daemon logs (Linux)
journalctl --user -u git-copy -fThe daemon:
- Auto-discovers repos with
.git-copy/config.jsonin your home directory - Polls every 30 seconds for changes
- Logs sync activity with commit hashes and target URLs
- Reloads config each cycle to pick up new repos
The install command automatically sets up:
- Linux: systemd user service (
~/.config/systemd/user/git-copy.service) - macOS: launchd agent (
~/Library/LaunchAgents/com.obinnaokechukwu.git-copy.plist)
After git-copy init, you'll be prompted to install the daemon for auto-sync.
- Fast Export: Uses
git fast-exportto stream the entire Git history - Streaming Filter: Processes each commit, blob, and ref in the stream
- Scrubbing:
- Replaces
private_usernamewithreplacementin all text - Applies
extra_replacements - Excludes files matching
excludepatterns (unless inopt_in) - Replaces
replace_history_with_currentfiles with HEAD content - Rewrites author/committer information
- Replaces
- Empty Commit Pruning: Commits with no remaining file operations are automatically dropped
- Fast Import: Imports the scrubbed stream into a temporary bare repo
- Validation: Checks for leaked private username or forbidden files
- Push Mirror: Force-pushes all refs to the target repository
- Validation: Automatically validates scrubbed repos for:
- Presence of private username in any file
- Forbidden files (
.env,CLAUDE.mdby default)
- Audit (history-aware):
git-copy syncaudits the scrubbed mirror by default to detect forbidden paths/strings anywhere in reachable history (and can optionally audit the remote mirror) - Non-negotiable Exclusions:
.git-copy/**and.claude/**are always excluded - Opt-In Override: Files in
opt_inbypassexcludepatterns - Author Protection: Rewrites commit authors to prevent identity leakage
- Atomic Updates: Uses temporary repos and atomic rename for safe caching
- Open-sourcing Private Repos: Scrub internal references before making code public
- Multi-Account Publishing: Maintain one private repo, sync to multiple public accounts
- Compliance: Ensure sensitive files never reach public repositories
- Brand Consistency: Replace internal names with public branding
- Personal Privacy: Separate work identity from public contributions
- License Changes: Retroactively change LICENSE to appear as if it was always Apache/MIT/etc.
- Pristine History: Make the public repo look like it was always public-ready
# Build
go build -o git-copy ./cmd/git-copy
# Run tests
go test ./...
# Install locally
go install ./cmd/git-copyLicensed under the Apache License, Version 2.0. See LICENSE for details.
Copyright 2026 obinnaokechukwu
Contributions welcome! Please open an issue or submit a pull request.
Important: Always review the scrubbed repository before syncing to ensure no sensitive data leaks. While git-copy includes validation, it's not foolproof.
- Test with
--repoflag on a copy first - Review
.git-copy/config.jsoncarefully - Check excluded patterns cover all sensitive paths
- Verify
extra_replacementscatches domain-specific secrets