Configuration¶
gitbulk reads two files, by default from ~/.config/gitbulk/:
| File | Purpose |
|---|---|
repos.txt |
The fleet — one repo per line. |
gitbulk.yaml |
Policy (merge/stale rules) and bot/human classification. |
Annotated examples ship in the repo's config/
directory (repos.txt.example, gitbulk.yaml.example). Copy them into place
and edit:
mkdir -p ~/.config/gitbulk
cp config/repos.txt.example ~/.config/gitbulk/repos.txt
cp config/gitbulk.yaml.example ~/.config/gitbulk/gitbulk.yaml
repos.txt — the fleet¶
One repo per line; blank lines and # comments (including inline) are ignored.
Three forms can be mixed freely:
- Canonical slug —
owner/repo. The local clone is assumed at<code-root>/<repo-name>(default code-root~/code/, override with--code-root PATH). - GitHub URL — HTTPS or SSH; gitbulk parses the slug from it.
- Local path — gitbulk runs
git -C <path> remote get-url originto discover the slug and pins the clone location to that path. Use this when your clones aren't organized by basename.
# Provenant
provenant-dev/origin-platform
https://github.com/provenant-dev/origin-agent-svc
~/code/work/special/nested-clone
Entries that can't be canonicalized to owner/repo (non-github.com hosts,
missing directories, repos without an origin remote) fail with a friendly
error. Duplicate slugs: first wins, the rest are warned and skipped.
gitbulk.yaml — policy and classification¶
Every key is optional; missing keys fall back to documented defaults. Unknown
keys are rejected loudly — a typo like min_buisness_days fails validation
rather than silently doing nothing.
defaults — merge and stale policy¶
The policy applied to every repo unless overridden. The most important keys:
| Key | Default | Meaning |
|---|---|---|
merge_policy |
strict |
strict | ci-only | never — how strict the merge gate is. |
merge_method |
rebase |
rebase | merge | squash, passed to gh pr merge. |
min_business_days |
3 |
Business days (Mon–Fri, local TZ) a PR must be "ready" before merge. |
unresolved_burden |
me |
me | other | either — who must mark review threads resolved. |
bot_threads_block |
true |
Whether unresolved bot threads also block "ready". |
stale_age_days |
90 |
close-stale inactivity threshold. |
stale_cooloff_days |
7 |
Minimum time since the warning before close-stale closes. |
stale_policy |
warn-and-close |
warn-and-close | warn-only | never. |
humans and bots — who counts as a reviewer¶
gitbulk needs to tell human reviewers from bots when it decides whether a PR is
"ready". It enumerates your org's members via gh api orgs/<org>/members
(cached for cache_ttl_hours, default 7 days), and treats the logins listed
under bots as non-human. Use humans.exceptions for org members that are
actually bots, and humans.always_human for humans outside the org.
repos — per-repo overrides¶
Keys are owner/repo strings from repos.txt. Scalar fields override the
matching defaults. value; list fields (skip_checks, extra_checks,
sacred_branches) are appended to the defaults rather than replacing them.
repos:
provenant-dev/origin-experimental-svc:
merge_policy: ci-only
min_business_days: 1
provenant-dev/origin-platform:
unresolved_burden: other # acting as maintainer here, not contributor
sacred_branches: [release/prod] # never let prune-worktrees sweep this one
sacred_branches — branches the prune commands must never delete¶
Both prune commands already refuse to delete a branch named main/master,
gh-pages/tick (well-known orphan-branch conventions — a GitHub Pages publish
branch and the tick defect-ledger branch),
or one matching a repo's GitHub default branch (prune-branches also
honours GitHub branch protection). They additionally never harvest any branch
that shares no history with the default branch — an orphan branch with no
common ancestor — even when it isn't named above. Set defaults.sacred_branches
(or a per-repo override) to extend that always-protected set with your own
conventions — develop, trunk, release, integration branches, etc.
The same set applies to both prune-worktrees (local branch/worktree
removal) and prune-branches (remote branch deletion): a name you protect from
local deletion is equally protected from remote deletion. Matching is exact and
case-sensitive, and the configured names are unioned with the built-in
protections, so this can only ever keep more branches.
See config/gitbulk.yaml.example
for the complete annotated reference, including the disposable
worktree_root.
agents / default_agent — which coding agent to drive¶
dispatch and summarize shell out to a CLI coding agent. By default that is
Claude Code (the claude preset), and if you set nothing here, behavior is
identical to before this feature existed. You can point gitbulk at a different
agent with one line, or define a fully custom one. The full design and security
model live in pluggable-agents.md.
default_agent: gemini # built-in presets: claude | gemini | copilot | cursor
agents:
gemini:
model: gemini-2.5-pro # override just one field of a preset
myagent: # a fully custom backend
command: [mytool, run, "{prompt}"] # argv LIST (never a shell string)
model_args: [--model, "{model}"] # appended only when a model is set
prompt_via: arg # arg | stdin
env: [MYTOOL_API_KEY] # allowlist — see below
sandbox: fs+no-net # none | fs-only | fs+no-net
repos:
someorg/some-repo:
agent: copilot # per-repo override
Selection order: --agent flag → per-repo agent: → default_agent →
claude.
Security-relevant fields:
commandis an argv list, never a shell string (a string is rejected).{prompt}/{model}substitute within a single token, so prompt text can't inject arguments, and the binary is pinned viawhichat load.envis an allowlist: only the named variables (plus a minimal safe base —PATH,HOME, locale) are passed to the agent. Omit it to inherit the full environment (the backward-compatible default, which hands the agent yourGH_TOKEN/SSH/cloud creds — prefer an allowlist for non-Claude agents).sandboxruns the agent in an unprivileged bubblewrap namespace.fs+no-netis the tightest (no network;~/.ssh/~/.aws/other clones not mounted) and is appropriate for conflict-resolution, which gitbulk arranges to need no network or credentials. Requiresbwrap+ unprivileged user namespaces; see running unattended. When a requested sandbox isn't available, the top-levelsandbox_fallback(refusedefault, orwarn-run) decides whether gitbulk refuses to run or runs unsandboxed with a warning.
Next steps¶
With both files in place, run gitbulk report to see
your fleet's PR state, then read it back with gitbulk show.