~/.config/git-cleanup/config.json.gitcleanup.json or .git-cleanup.json, found by walking up from the current directory — perfect for per-repo rules--config <file>Invalid values are rejected loudly at startup — never silently ignored.
Editors get autocomplete and inline validation from the JSON Schema at
support/config.schema.json (shipped in the npm package): add
"$schema": "https://raw.githubusercontent.com/Asunachi/git-cleanup/main/support/config.schema.json"
to your config file, or register the schema with your editor's JSON settings.
{
// Globs never touched, on top of the built-in protected list.
"protected": ["special/release", "vendor/**"],
// Merged branches at least this old (days, from the tip commit) are prunable.
"deleteMergedAfterDays": 21,
// Unmerged branches older than this are flagged stale (never deleted).
"warnUnmergedAfterDays": 45,
// Per-name rules. mode "merged" = custom age for merged branches;
// mode "any" = force-delete even when unmerged (local only — opt in!).
"rules": [
{ "match": "feature/ci-*", "mode": "merged", "minAgeDays": 7 },
{ "match": "tmp/**", "mode": "any", "minAgeDays": 1 }
],
"pr": {
"track": true, // enrich branches with PR state
"staleAfterDays": 30, // open PR with no activity = stale
"closeStaleAfterDays": 60, // >0 enables `prs --close`
"closeComment": "Auto-closed by git-cleanup — reopen if still needed."
},
"remote": {
"pruneMerged": true, // prune --remote deletes merged remote branches
"deleteAbandonedAfterDays": 0 // >0: delete remote branches whose PR closed unmerged
},
"forge": {
"hosts": { // claim self-hosted forge hostnames
"git.example.com": "gitlab", // (PR tracking + report-issue)
"git.internal": "gitea"
}
},
"backup": {
"enabled": true, // bundle refs before -D / remote deletions
"dir": null, // null = <git dir>/git-cleanup-backups
"retainDays": 0 // 0 = keep forever; >0 sweeps older bundles
},
"repos": ["../other-project", { "path": "/srv/legacy", "mode": "prune" }],
"sweep": {
"mode": "report", // "report" never deletes | "prune"
"remote": false, // also delete remote branches during sweep
"reportFile": null, // combined markdown report path, or null
"reportIssue": null // true, { "title": "..." }, or null
}
}
The first matching rule for a branch wins.
mode: "merged" — fires only for merged branches, uses
minAgeDays (default deleteMergedAfterDays). An unmerged
branch matching one is kept with reason unmerged-rule. Can also
raise the generic threshold for a glob.mode: "any" — fires on unmerged branches too, with
minAgeDays defaulting to 0. Pair with a meaningful age — this is
the only way the tool deletes unmerged work, and it only affects local
branches.
Base refs (from the remote's HEAD), the checked-out branch, and any name
matching the built-in protected list or your protected globs are
never pruned. The built-in list covers version-shaped names like
v1, 5.x, release/*, and the common
main/master/develop/dev — add your own
with the protected globs.
| Variable | Purpose |
|---|---|
GITHUB_TOKEN / GITLAB_TOKEN / BITBUCKET_TOKEN / GITEA_TOKEN | PR enrichment + report-issue auth |
GITLAB_API_BASE / BITBUCKET_API_BASE / GITEA_API_BASE / GITHUB_API_BASE | API base overrides (self-hosted forges) |
CI_API_V4_URL | GitLab report-issue API base (set by GitLab CI) |
CI_JOB_TOKEN | GitLab report-issue fallback auth inside GitLab CI |
GIT_CLEANUP_FETCH_TIMEOUT_MS | forge API timeout (default 15000) |
GIT_CLEANUP_YES=1 | same as --yes |
GIT_CLEANUP_NO_COLOR=1 | disable ANSI colors (NO_COLOR also works) |