Configuration
Config file
Generate a default config:
repos-manager init This creates ~/.config/repos-manager/config.json. The path is the same on every OS, including Windows (%USERPROFILE%\.config\repos-manager\config.json), and can be overridden with REPOS_MANAGER_CONFIG.
{
"base_dir": "~/Documents",
"parallel": 4,
"protocol": "ssh",
"check_updates": true,
"scan_network_mounts": false,
"gitlab_statistics": true,
"confirm_sync": "auto",
"confirm_new_repos": 5,
"confirm_download_gb": 1,
"hosts": {
"github": ["github.com"],
"gitlab": ["gitlab.com"],
"forgejo": ["codeberg.org"],
"bitbucket": ["bitbucket.org"]
},
"repos": {}
} | Key | Description | Default |
|---|---|---|
base_dir | Root directory for all repos (~ is expanded, must resolve to an absolute path) | ~/Documents |
parallel | Number of parallel sync jobs | 4 |
protocol | Clone protocol (ssh or https) | ssh |
check_updates | Check for a newer release and show a banner | true |
scan_network_mounts | Let status traverse FUSE, NFS or SMB mounts under base_dir (see warning below) | false |
gitlab_statistics | Ask GitLab for repository sizes. The full project serializer costs about 6 times the bytes and twice the time; set false to turn it off, sizes then stay unknown | true |
confirm_sync | When a sync asks before cloning: auto, always or never (see below) | auto |
confirm_new_repos | New clones, counted across the whole run, that make auto ask | 5 |
confirm_download_gb | Gigabytes to download, counted across the whole run, that make auto ask | 1 |
hosts.<provider> | List of hosts to sync for that provider | See defaults below |
repos.<host> | Plain git servers with no forge API, synced by the any provider | Empty |
An invalid JSON file is reported as an error instead of being silently ignored.
Update banner
When check_updates is enabled, login, sync and status read a cached "latest version" and print a single-line yellow banner when it is newer than the running binary. If the cache is older than 24 hours, a detached background copy of repos-manager refreshes it from the GitHub Releases API. The check never blocks the command in progress and stays silent when nothing new is available or the network is down.
Opt out at any time without editing the config file:
REPOS_MANAGER_NO_UPDATE_CHECK=1 repos-manager status Sync confirmation
Before the first clone, sync reports what it is about to do, and asks once when the run is big (see Usage, Before it clones). These three keys decide when that happens. They describe the machine, which is why they live here and not in the workspace's .repos.yml: what to sync is a property of the directory, how careful to be before downloading is a property of the computer you are on.
{
"confirm_sync": "auto",
"confirm_new_repos": 5,
"confirm_download_gb": 1
} confirm_sync | Behaviour |
|---|---|
auto (default) | Summarize and ask only when the run crosses confirm_new_repos or confirm_download_gb |
always | Ask as soon as there is anything to clone or update |
never | Never ask. A run over the thresholds is still summarized, so the information is not lost |
Both thresholds are counted over the whole run, not per host, so sync --all asks once or not at all. A laptop on a metered connection is a good place for "confirm_download_gb": 0.2, a workstation on fibre for "confirm_sync": "never".
Whatever the mode, the question is only ever asked when stdin and stdout are both terminals: a scheduled sync or a CI job prints the summary and carries on. --yes and --dry-run also print the summary without asking.
Network-mount scanning
repos-manager status walks base_dir recursively to collect every .git directory. Cloud drives mounted via FUSE (kDrive, Dropbox, sshfs, and similar) and network filesystems (NFS, SMB/CIFS, davfs, Ceph, AFS) turn each readdir into a blocking round-trip, and a single repo on such a mount can hang the scan for minutes.
On Linux, status reads /proc/self/mountinfo by default, finds every mount point under base_dir whose filesystem type matches fuse, fuse.*, nfs, cifs, smb*, smbfs, afs, ceph or davfs, and prunes it from the walk. Use --verbose to see which mounts were skipped. On macOS and Windows there is no such detection.
Set scan_network_mounts: true only if you really do host working copies on a reliable network share. The scan will then descend into those mounts and print a warning that this can cause long hangs on flaky links.
GitLab repository sizes
GitLab only reports a repository size when the listing uses its full project serializer (statistics=true), which makes the payload heavier than the simple=true listing used before. Measured on gitlab.com with 38 member projects on 2026-10-11:
| Listing | Payload | Fields per project | Wall clock, median of 3 |
|---|---|---|---|
simple=true | 34 KB | 20 | 1.1 s |
statistics=true | 205 KB | 123 | 2.1 s |
So about 6 times the bytes and twice the wall clock, for one request per 100 projects either way. On a very large GitLab, set "gitlab_statistics": false to go back to the light listing: every GitLab repository then has an unknown size and the recon summary shows ?. Projects where you are below the Reporter role have an unknown size either way.
Multiple hosts per provider
Each provider key under hosts accepts an array of hostnames, so you can sync several GitHub, GitLab or Forgejo instances side-by-side (typical setup: a SaaS one + your self-hosted one).
{
"hosts": {
"gitlab": ["gitlab.com", "gitlab.example.org"],
"forgejo": ["codeberg.org", "forge.example.org"]
}
} repos-manager gitlab sync and repos-manager sync --all will then iterate over every configured host. Repositories are mirrored under <base_dir>/<host>/..., so each instance gets its own directory tree and its own sync lock.
The legacy string form ("gitlab": "gitlab.com") is still accepted and treated as a single-element list. gitea and gogs are accepted as aliases of forgejo, and srht as an alias of sourcehut.
Per-CLI requirements for self-hosted instances:
- GitHub Enterprise:
repos-managerpasses--hostname <host>togh. Rungh auth login --hostname <host>once per host. - GitLab:
repos-managersetsGITLAB_HOSTsoglabtargets the right instance. Runglab auth login --hostname <host>once per host. - Forgejo / Gitea / Gogs:
repos-managerreads the matching login entry (URL + token) from tea's config file and talks to the Forgejo REST API directly. Runtea login addonce per instance, orrepos-manager forgejo loginto save a base URL and a token inforgejo-credswhenteais missing or refuses the server, which is how Gogs is reached. Atealogin wins over a saved token; when neither matches, the host is skipped with a warning and the sync continues with the next one. - Bitbucket:
bitbucket.orggoes through the Cloud 2.0 API with an Atlassian API token; any other host is treated as Data Center / Server and goes through the 1.0 API with an HTTP access token. Runrepos-manager bitbucket loginonce per host. - Sourcehut:
repos-managerpasses--instance <host>tohut. Runhut initonce per instance. - OneDev: credentials are stored per host by
repos-manager onedev login. OneDev is self-hosted only, so it has no default host. - Radicle: the host is only used for the directory name; listing always targets your local Radicle node.
- Plain git servers (
any): hosts come from thereposkey, not fromhosts, and are reached with your own SSH keys. See below.
You can still override the configured list for a one-off sync with repos-manager <provider> sync --host <hostname>.
Defaults
If hosts is missing or empty for a provider, repos-manager falls back to its default host, which is the SaaS one where there is one:
| Provider | Default host |
|---|---|
github | github.com |
gitlab | gitlab.com |
forgejo | codeberg.org |
bitbucket | bitbucket.org |
radicle | radicle (local node) |
sourcehut | git.sr.ht |
onedev | None, self-hosted only |
any | None, the keys of repos |
onedev has no default: nothing is synced until you add "hosts": {"onedev": ["onedev.example.org"]}. The any provider takes its hosts from the keys of the repos key described below, so it needs no hosts entry either.
Plain git servers (repos)
A bare git repository served over SSH has no forge API to ask, so the any provider reads the top-level repos key instead. Its keys are hosts, and each key doubles as the directory name under base_dir.
An entry is either a list of clone URLs, or an object with urls, discover, user and port. Both forms are accepted.
Clone URLs listed by hand
{
"repos": {
"git.example.org": [
"git@git.example.org:infra/ansible.git",
"ssh://me@vps.example.org:2222/srv/git/notes.git",
"https://git.example.org/a/b/c.git"
]
}
} The local path comes from the URL: the three above land in infra/ansible, srv/git/notes and a/b/c under their host directory. A listed URL is used exactly as written, so --https does not rewrite it.
gitolite
{ "repos": { "gitolite.example.org": { "discover": "gitolite", "user": "git" } } } Runs ssh [-p port] <user>@<host> info and reads the repository list out of it. user defaults to git, port is optional. Wildcard entries of the gitolite config (patterns, CREATOR) are not repositories and are dropped.
Soft Serve
{ "repos": { "soft.example.org": { "discover": "soft-serve", "port": 23231 } } } Runs ssh -p <port> <host> repo list. Port defaults to 23231, Soft Serve's default.
A directory of bare repositories
{ "repos": { "vps.example.org": { "discover": "find:/srv/git", "user": "me", "port": 2222 } } } Runs find /srv/git -name '*.git' -prune over SSH. This one needs a real shell on the remote account: it does not work with git-shell unless a list script sits in ~/git-shell-commands.
Both at once
urls and discover can be combined in one entry, and the union is synced:
{
"repos": {
"vps.example.org": {
"urls": ["ssh://me@vps.example.org:2222/srv/git/legacy.git"],
"discover": "find:/srv/git",
"user": "me",
"port": 2222
}
}
} Every discovery command runs with BatchMode=yes and a connect timeout, so an unknown host key or a passphrase with no agent fails with a message instead of blocking the sync. There is nothing to log in to: these hosts use your own SSH keys.
Credential files
Three providers store their own credentials next to the config file, under ~/.config/repos-manager/. Each one is a JSON map keyed by host, created with mode 600 on Linux and macOS, so several instances of the same provider coexist.
| File | Written by | Holds |
|---|---|---|
bitbucket-creds | repos-manager bitbucket login | Atlassian API token (Cloud, with the account email) or HTTP access token (Data Center) |
forgejo-creds | repos-manager forgejo login | Base URL and access token, used when tea is missing or declined |
onedev-creds | repos-manager onedev login | Account name and access token per OneDev instance |
GitHub, GitLab, Forgejo through tea, Sourcehut and Radicle keep their credentials in their own CLI's configuration, and the any provider has none. See the Providers page for the details of each file.
Environment variables
| Variable | Description | Default |
|---|---|---|
REPOS_MANAGER_CONFIG | Path to config file | ~/.config/repos-manager/config.json |
REPOS_MANAGER_BASE_DIR | Override base directory | ~/Documents |
REPOS_MANAGER_PARALLEL | Override parallel jobs | 4 |
REPOS_MANAGER_PROTOCOL | Override protocol (ssh or https) | ssh |
REPOS_MANAGER_NO_UPDATE_CHECK | Set to 1 to skip the background update check | Unset |
REPOS_MANAGER_UPDATE_TTL | Seconds between update checks (cache TTL) | 86400 |
REPOS_MANAGER_UPDATE_CACHE | Path to the cached latest-version file | <cache dir>/repos-manager/latest-version |
REPOS_MANAGER_GIT_TIMEOUT | Seconds before a single git command is killed | 3600 |
REPOS_MANAGER_LIST_TIMEOUT | Seconds before a provider CLI listing is killed | 300 |
REPOS_MANAGER_UPDATE_URL | Release API endpoint used by the update check and update | GitHub Releases API |
TEA_CONFIG | Path to tea's config file, read by the Forgejo provider | tea's default location (see Providers) |
NO_COLOR | Disable colored output | Unset |
The cache directory is $XDG_CACHE_HOME when set, otherwise the OS cache directory (~/.cache on Linux, ~/Library/Caches on macOS, %LOCALAPPDATA% on Windows).
Environment variables take precedence over the config file. Flags take precedence over both.