agmh 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1575 @@
1
+ Metadata-Version: 2.4
2
+ Name: agmh
3
+ Version: 0.2.0
4
+ Summary: AGMH, a local GitHub backup and repository mirroring CLI.
5
+ Author-email: extencil <extencil@segfault.net>
6
+ Maintainer-email: extencil <extencil@segfault.net>
7
+ License-Expression: Unlicense
8
+ Project-URL: Homepage, https://github.com/haltman-io/agmh
9
+ Project-URL: Repository, https://github.com/haltman-io/agmh
10
+ Project-URL: Issues, https://github.com/haltman-io/agmh/issues
11
+ Keywords: backup,git,github,mirror,repository
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Software Development :: Version Control :: Git
22
+ Requires-Python: >=3.11
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Provides-Extra: tui
26
+ Requires-Dist: rich>=13.7; extra == "tui"
27
+ Provides-Extra: dev
28
+ Requires-Dist: build>=1.2; extra == "dev"
29
+ Requires-Dist: twine>=5.1; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+ # AGMH
33
+
34
+ ![License: Unlicense](https://img.shields.io/badge/license-Unlicense-blue.svg)
35
+ ![Python](https://img.shields.io/badge/python-3.11%2B-3776ab.svg)
36
+ ![CLI](https://img.shields.io/badge/interface-CLI%20%2B%20TUI-111111.svg)
37
+ ![Sources](https://img.shields.io/badge/sources-GitHub%20%7C%20GitLab%20%7C%20Forgejo%20%7C%20Bitbucket%20%7C%20SourceHut-111111.svg)
38
+ ![Destinations](https://img.shields.io/badge/destinations-GitHub%20%7C%20GitLab%20%7C%20Forgejo%20%7C%20Bitbucket%20%7C%20SourceHut-111111.svg)
39
+
40
+ AGMH means **ANTI GITHUB & MICROSOFT HYSTERIA**.
41
+
42
+ Repository: [haltman-io/agmh](https://github.com/haltman-io/agmh)
43
+
44
+ AGMH is a local backup and repository mirroring CLI built to help researchers,
45
+ maintainers, and software teams pull their work out of a forge quickly and push
46
+ it to another forge without losing years of history, branches, tags, or research.
47
+
48
+ It discovers repositories from supported source profiles, organizations, groups,
49
+ namespaces, or workspaces; clones them locally as mirrors; adds a small
50
+ provenance marker file when enabled; creates matching repositories on
51
+ destination platforms; and pushes the backup to GitHub, GitLab,
52
+ Codeberg/Forgejo, SourceHut, Bitbucket, or compatible Git remotes.
53
+
54
+ The primary CLI command is:
55
+
56
+ ```bash
57
+ agmh
58
+ ```
59
+
60
+ The legacy typo `aghm` was removed. AGMH intentionally uses `agmh` for commands,
61
+ default config files, state directories, logs, and generated marker files.
62
+
63
+ ## Contents
64
+
65
+ - [Capabilities](#capabilities)
66
+ - [Supported Platforms](#supported-platforms)
67
+ - [System Requirements](#system-requirements)
68
+ - [Installation](#installation)
69
+ - [Quick Start](#quick-start)
70
+ - [Workflow Modes](#workflow-modes)
71
+ - [Input Files](#input-files)
72
+ - [Configuration](#configuration)
73
+ - [Operations](#operations)
74
+ - [Troubleshooting](#troubleshooting)
75
+ - [Security Notes](#security-notes)
76
+ - [Development](#development)
77
+ - [Project Background](#project-background)
78
+
79
+ ## Capabilities
80
+
81
+ AGMH can:
82
+
83
+ - Read source profile, organization, group, namespace, or workspace URLs from a text file.
84
+ - Discover accessible public and private repositories from GitHub, GitLab, Forgejo/Gitea, Bitbucket, and SourceHut.
85
+ - Use one or more source tokens to increase API limits and access private repos.
86
+ - Rotate tokens when rate limits or authorization failures happen.
87
+ - Clone each repository locally using `git clone --mirror`.
88
+ - Run in local-only mode to download/update mirrors without pushing anywhere.
89
+ - Run in remote-only mode to push existing local mirrors to configured destinations later.
90
+ - Keep a local backup under `backups/` by default.
91
+ - Optionally add `agmh.txt` to the default branch before remote mirroring.
92
+ - Create destination repositories through platform APIs.
93
+ - Preserve repository name and public/private visibility where supported.
94
+ - Push mirrors to GitHub, GitLab, Codeberg/Forgejo, SourceHut, Bitbucket, and similar Git destinations.
95
+ - Use resumable state in `.agmh/state.json`.
96
+ - Write detailed logs to `.agmh/logs/`.
97
+ - Run dry-run simulations.
98
+ - Use proxies.
99
+ - Disable TLS verification when needed for intercepting proxies.
100
+ - Use SSH keys for destinations such as SourceHut.
101
+ - Keep going when one repository fails instead of aborting the whole run.
102
+
103
+ ## Supported Platforms
104
+
105
+ Source support:
106
+
107
+ | Platform | Discovery scope | Private repos | Notes |
108
+ | --- | --- | --- | --- |
109
+ | GitHub | User or organization | Yes, with token | Uses GitHub REST repositories API. GitHub Enterprise can use `api_base`. |
110
+ | GitLab | User, group, or subgroup | Yes, with token | Group discovery includes subgroups. `internal` repositories are treated as non-public when mirrored elsewhere. |
111
+ | Codeberg | User or organization | Yes, with token | Uses the Forgejo adapter. |
112
+ | Forgejo/Gitea | User or organization | Yes, with token | Works with compatible `/api/v1` instances. |
113
+ | Bitbucket | Workspace | Yes, with token | Uses Bitbucket Cloud workspaces and repository pagination. |
114
+ | SourceHut | User | Yes, with token | Uses the git.sr.ht GraphQL API. `unlisted` visibility is preserved when the destination supports it. |
115
+
116
+ Destination support:
117
+
118
+ | Platform | API create | HTTPS push | SSH push | Notes |
119
+ | --- | --- | --- | --- | --- |
120
+ | GitHub | Yes | Yes | Possible with custom URL | Use a destination token with repo creation and push permissions. GitHub Enterprise can use `api_base`. |
121
+ | GitLab | Yes | Yes | Possible with custom URL | Hidden repo names such as `.github` are mapped to valid GitLab paths such as `dot-github`. |
122
+ | Codeberg | Yes | Yes | Possible with custom URL | Uses Forgejo API. GitHub `refs/pull/*` refs are excluded in portable mirror mode because Codeberg rejects hidden refs. |
123
+ | Forgejo/Gitea | Yes | Yes | Possible with custom URL | Same adapter family as Codeberg. |
124
+ | SourceHut | Yes | Optional | Yes | API token creates repositories, SSH is recommended for Git pushes. |
125
+ | Bitbucket | Yes | Yes | Possible with custom URL | Requires Bitbucket-compatible credentials. |
126
+
127
+ ## System Requirements
128
+
129
+ - Python 3.11 or newer.
130
+ - Git available in `PATH`.
131
+ - Network access to source and destination forges.
132
+ - Destination accounts and tokens with enough permission to create repositories and push Git refs.
133
+ - Optional: `git-lfs` if you enable `lfs = true`.
134
+ - Optional: `ssh-agent` and SSH keys for SourceHut or SSH-based destinations.
135
+
136
+ Ubuntu system packages:
137
+
138
+ ```bash
139
+ sudo apt update
140
+ sudo apt install -y python3 python3-venv python3-pip git ca-certificates openssh-client
141
+ ```
142
+
143
+ Optional packages:
144
+
145
+ ```bash
146
+ sudo apt install -y git-lfs curl
147
+ ```
148
+
149
+ Notes:
150
+
151
+ - `git-lfs` is only required when AGMH is configured with `lfs = true`.
152
+ - `curl` is used only for the troubleshooting and proxy test commands shown in this README.
153
+ - If your Ubuntu release provides Python older than 3.11, install Python 3.11 or newer before creating the virtual environment.
154
+
155
+ ## Installation
156
+
157
+ Clone the repository:
158
+
159
+ ```bash
160
+ git clone https://github.com/haltman-io/agmh.git
161
+ cd agmh
162
+ ```
163
+
164
+ Create a virtual environment:
165
+
166
+ ```bash
167
+ python3 -m venv .venv
168
+ source .venv/bin/activate
169
+ ```
170
+
171
+ Install AGMH in editable mode:
172
+
173
+ ```bash
174
+ python -m pip install -U pip
175
+ python -m pip install -e ".[tui]"
176
+ ```
177
+
178
+ Check the CLI:
179
+
180
+ ```bash
181
+ agmh --help
182
+ agmh run --help
183
+ ```
184
+
185
+ If you do not install the package, you can run it with `PYTHONPATH`:
186
+
187
+ ```bash
188
+ PYTHONPATH=src python3 -m anti_gh_ms_hysteria run --help
189
+ ```
190
+
191
+ ## Quick Start
192
+
193
+ Create a starter config:
194
+
195
+ ```bash
196
+ cp config.example.toml agmh.config.toml
197
+ ```
198
+
199
+ Create `sources.txt` from the public-safe example:
200
+
201
+ ```bash
202
+ cp sources.example.txt sources.txt
203
+ $EDITOR sources.txt
204
+ ```
205
+
206
+ If you prefer to keep destinations in a separate file instead of inline TOML,
207
+ start from the destination example:
208
+
209
+ ```bash
210
+ cp destinations.example.txt destinations.txt
211
+ $EDITOR destinations.txt
212
+ ```
213
+
214
+ Export tokens:
215
+
216
+ ```bash
217
+ export GITHUB_TOKEN="github_token_here"
218
+ export GITHUB_DEST_TOKEN="github_destination_token_here"
219
+ export GITLAB_TOKEN="gitlab_token_here"
220
+ export CODEBERG_TOKEN="codeberg_token_here"
221
+ export SOURCEHUT_TOKEN="sourcehut_token_here"
222
+ ```
223
+
224
+ Run a dry-run first:
225
+
226
+ ```bash
227
+ agmh run --config agmh.config.toml --dry-run --verbose
228
+ ```
229
+
230
+ Run the real backup:
231
+
232
+ ```bash
233
+ agmh run --config agmh.config.toml --verbose
234
+ ```
235
+
236
+ Check state:
237
+
238
+ ```bash
239
+ agmh state --config agmh.config.toml
240
+ ```
241
+
242
+ ## Workflow Modes
243
+
244
+ Use this table to choose the right command:
245
+
246
+ | Goal | Command | Reads sources | Pushes destinations |
247
+ | --- | --- | --- | --- |
248
+ | Download mirrors and push them now | `agmh run --config agmh.config.toml` | Yes | Yes |
249
+ | Download or update local mirrors only | `agmh local-mirror --config agmh.config.toml` | Yes | No |
250
+ | Push mirrors that already exist locally | `agmh remote-mirror --config agmh.config.toml` | No | Yes |
251
+ | Keep polling sources and react to changes | `agmh watching --config agmh.config.toml` | Yes | Depends on `watch.action` |
252
+
253
+ Default full workflow:
254
+
255
+ ```bash
256
+ agmh run --config agmh.config.toml --verbose
257
+ ```
258
+
259
+ This discovers source repositories, clones or updates local mirrors, adds the
260
+ marker commit when `backup.marker_enabled` is true, creates destination
261
+ repositories, and pushes mirrors.
262
+
263
+ Local mirror only:
264
+
265
+ ```bash
266
+ agmh local-mirror --config agmh.config.toml --verbose
267
+ ```
268
+
269
+ Equivalent:
270
+
271
+ ```bash
272
+ agmh run --config agmh.config.toml --mode local --verbose
273
+ ```
274
+
275
+ This discovers source repositories and only clones or updates local bare mirrors
276
+ under `backup.local_dir`. It does not create marker commits and does not contact
277
+ destination forges, even if destinations are present in the config.
278
+
279
+ Remote mirror from existing local mirrors:
280
+
281
+ ```bash
282
+ agmh remote-mirror --config agmh.config.toml --verbose
283
+ ```
284
+
285
+ Equivalent:
286
+
287
+ ```bash
288
+ agmh run --config agmh.config.toml --mode remote --verbose
289
+ ```
290
+
291
+ This does not discover or clone from source forges. It reads mirrors recorded in
292
+ `.agmh/state.json`, falls back to scanning `backup.local_dir`, adds the marker
293
+ commit if enabled and needed, creates destination repositories, and pushes the
294
+ local mirrors.
295
+ When AGMH has to scan local mirrors without state metadata, repository privacy is
296
+ unknown, so it treats those repositories as private by default.
297
+
298
+ By default, remote mirror follows the source repository visibility. You can
299
+ override destination visibility for the whole remote mirror run:
300
+
301
+ ```bash
302
+ agmh remote-mirror --config agmh.config.toml --destination-visibility mirror
303
+ agmh remote-mirror --config agmh.config.toml --destination-visibility public
304
+ agmh remote-mirror --config agmh.config.toml --destination-visibility private
305
+ ```
306
+
307
+ `mirror` applies the same visibility recorded from the source. `public` creates
308
+ destination repositories as public regardless of source visibility. `private`
309
+ creates destination repositories as private regardless of source visibility. If
310
+ a destination repository already exists, AGMH uses the existing repository as-is.
311
+ The same override is available with `agmh run --mode remote`.
312
+
313
+ Watching mode:
314
+
315
+ ```bash
316
+ agmh watching --config agmh.config.toml --verbose
317
+ ```
318
+
319
+ Equivalent:
320
+
321
+ ```bash
322
+ agmh run --config agmh.config.toml --mode watching --verbose
323
+ ```
324
+
325
+ Watching mode runs until interrupted. It polls enabled sources, compares the
326
+ current source metadata with `.agmh/state.json`, and runs the configured action
327
+ only for first-seen or changed repositories. The default action is `full`.
328
+ The change fingerprint uses source API update fields such as GitHub `pushed_at`,
329
+ GitLab `last_activity_at`, Forgejo `updated_at`, Bitbucket `updated_on`, and
330
+ SourceHut `updated`. GitLab documents that `last_activity_at` can lag by up to
331
+ one hour, so GitLab polling is eventually consistent rather than instant.
332
+
333
+ Watching actions:
334
+
335
+ | Action | Behavior |
336
+ | --- | --- |
337
+ | `full` | Clone/update the local mirror, ensure the marker when enabled, create destinations, and push. |
338
+ | `local` | Clone/update the local mirror only. |
339
+ | `remote` | Push an existing local mirror for the changed repository. If the local mirror is missing, the action fails for that repository. |
340
+
341
+ Configure polling globally:
342
+
343
+ ```toml
344
+ [watch]
345
+ interval_seconds = 300
346
+ action = "full" # full, local, or remote
347
+ initial_run = true # process repositories the first time they are seen
348
+ once = false # useful for tests or supervised one-shot runs
349
+ ```
350
+
351
+ Override polling per source:
352
+
353
+ ```toml
354
+ [[sources]]
355
+ url = "https://gitlab.com/haltman-io"
356
+ platform = "gitlab"
357
+ tokens = [{ env = "GITLAB_SOURCE_TOKEN" }]
358
+ watch = true
359
+ watch_interval_seconds = 120
360
+ watch_action = "local"
361
+ ```
362
+
363
+ CLI overrides:
364
+
365
+ ```bash
366
+ agmh watching \
367
+ --config agmh.config.toml \
368
+ --watch-interval 120 \
369
+ --watch-action full
370
+ ```
371
+
372
+ Use `--no-watch-initial-run` when you want AGMH to record the current state
373
+ without processing existing repositories on the first polling cycle. Use
374
+ `--watch-once` to run one polling cycle and exit.
375
+
376
+ You can also set the mode in TOML:
377
+
378
+ ```toml
379
+ mode = "full" # full, local, remote, or watching
380
+ ```
381
+
382
+ ## Input Files
383
+
384
+ AGMH reads source profiles from a plain text file. Use one profile,
385
+ organization, group, namespace, or workspace URL per line:
386
+
387
+ ```txt
388
+ https://github.com/extencil/
389
+ https://github.com/haltman-io/
390
+ https://gitlab.com/haltman-io/
391
+ https://codeberg.org/haltman/
392
+ https://bitbucket.org/example-workspace/
393
+ https://git.sr.ht/~extencil/
394
+ ```
395
+
396
+ Blank lines and lines beginning with `#` are ignored.
397
+
398
+ You can also pass source profiles directly:
399
+
400
+ ```bash
401
+ agmh run --source https://github.com/extencil/ --source https://github.com/haltman-io/
402
+ ```
403
+
404
+ For private non-GitHub sources, prefer inline `[[sources]]` entries so each
405
+ source can declare its own `tokens` and `api_base`.
406
+
407
+ Destinations can be configured in TOML or in a plain text file:
408
+
409
+ ```txt
410
+ https://gitlab.com/haltman-io
411
+ https://codeberg.org/haltman
412
+ https://git.sr.ht/~extencil
413
+ ```
414
+
415
+ ## Configuration
416
+
417
+ Most users should start from `config.example.toml`, then edit `sources_file`,
418
+ `[[sources]]`, and `[[destinations]]` for their environment. Use environment
419
+ variables for tokens and webhook URLs.
420
+
421
+ ### Full Example
422
+
423
+ ```toml
424
+ workspace = ".agmh"
425
+ mode = "full"
426
+ dry_run = false
427
+ verbose = 0
428
+ tui = true
429
+ insecure_tls = false
430
+ sources_file = "sources.txt"
431
+
432
+ [github]
433
+ tokens = [
434
+ { env = "GITHUB_TOKEN", name = "github-primary" },
435
+ # { env = "GITHUB_TOKEN_2", name = "github-secondary" },
436
+ ]
437
+
438
+ [watch]
439
+ interval_seconds = 300
440
+ action = "full"
441
+ initial_run = true
442
+ once = false
443
+
444
+ [notifications]
445
+ enabled = false
446
+ events = ["*"]
447
+ fail_silently = true
448
+ timeout_seconds = 10
449
+
450
+ # [[webhooks]]
451
+ # name = "ops-discord"
452
+ # platform = "discord"
453
+ # url_env = "DISCORD_WEBHOOK_URL"
454
+ # events = ["start", "finish", "error", "local_saved", "remote_saved", "watch_check", "watch_update", "watch_none"]
455
+ # username = "AGMH"
456
+ #
457
+ # [[webhooks]]
458
+ # name = "ops-telegram"
459
+ # platform = "telegram"
460
+ # bot_token_env = "TELEGRAM_BOT_TOKEN"
461
+ # chat_id_env = "TELEGRAM_CHAT_ID"
462
+ # events = ["start", "finish", "error", "watch_update"]
463
+ # parse_mode = "HTML"
464
+
465
+ # Inline sources are useful when a non-GitHub source needs a token or api_base.
466
+ [[sources]]
467
+ url = "https://gitlab.com/haltman-io"
468
+ platform = "gitlab"
469
+ tokens = [{ env = "GITLAB_SOURCE_TOKEN", name = "gitlab-source" }]
470
+ watch_interval_seconds = 120
471
+ watch_action = "local"
472
+
473
+ [backup]
474
+ local_dir = "backups"
475
+ clone_protocol = "https"
476
+ include_archived = true
477
+ include_forks = true
478
+ include_private_for_authenticated_user = true
479
+ lfs = false
480
+ marker_enabled = true
481
+ push_mode = "mirror"
482
+
483
+ [retry]
484
+ max_retries = 5
485
+ base_delay_seconds = 1.5
486
+ max_delay_seconds = 60
487
+ request_timeout_seconds = 15
488
+ rate_limit_sleep_seconds = 300
489
+ wait_on_rate_limit = true
490
+
491
+ [git]
492
+ author_name = "extencil"
493
+ author_email = "extencil@segfault.net"
494
+ commit_message = "Backuping with AGMH v{version}"
495
+ # ssh_identity_file = "/home/user/.ssh/sourcehut_ed25519"
496
+ # ssh_identities_only = true
497
+ # ssh_batch_mode = false
498
+ # ssh_strict_host_key_checking = "accept-new"
499
+ # ssh_command = "ssh -i /home/user/.ssh/sourcehut_ed25519 -o IdentitiesOnly=yes"
500
+
501
+ [[destinations]]
502
+ url = "https://github.com/haltman-io-mirror"
503
+ platform = "github"
504
+ tokens = [{ env = "GITHUB_DEST_TOKEN", name = "github-destination" }]
505
+ visibility = "mirror"
506
+ push_mode = "mirror"
507
+
508
+ [[destinations]]
509
+ url = "https://gitlab.com/haltman-io"
510
+ platform = "gitlab"
511
+ tokens = [{ env = "GITLAB_TOKEN", name = "gitlab-primary" }]
512
+ visibility = "mirror"
513
+ push_mode = "mirror"
514
+
515
+ [[destinations]]
516
+ url = "https://codeberg.org/haltman"
517
+ platform = "forgejo"
518
+ tokens = [{ env = "CODEBERG_TOKEN", name = "codeberg-primary" }]
519
+ visibility = "mirror"
520
+ push_mode = "mirror"
521
+
522
+ [[destinations]]
523
+ url = "https://git.sr.ht/~extencil"
524
+ platform = "sourcehut"
525
+ tokens = [{ env = "SOURCEHUT_TOKEN", name = "sourcehut-primary" }]
526
+ visibility = "mirror"
527
+ push_mode = "mirror"
528
+ push_url_template = "git@git.sr.ht:~{owner}/{repo}"
529
+ ```
530
+
531
+ ### Reference
532
+
533
+ Top-level options:
534
+
535
+ | Key | Meaning |
536
+ | --- | --- |
537
+ | `mode` | Workflow mode: `full`, `local`, `remote`, or `watching`. Default: `full`. |
538
+ | `workspace` | Local state and logs directory. Default: `.agmh`. |
539
+ | `dry_run` | Plan actions without cloning, creating, or pushing. |
540
+ | `verbose` | Default verbosity level. CLI `-v` can override it. |
541
+ | `tui` | Use Rich console rendering when installed. |
542
+ | `proxy` | Optional HTTP/HTTPS proxy URL. |
543
+ | `insecure_tls` | Disable TLS certificate verification for API calls and Git HTTPS operations. |
544
+ | `resume` | Reuse `.agmh/state.json` and skip completed steps. |
545
+ | `force` | Redo steps even if state says they are complete. |
546
+ | `sources_file` | Text file containing source profile/org/group/workspace URLs. |
547
+
548
+ GitHub source shortcut options:
549
+
550
+ | Key | Meaning |
551
+ | --- | --- |
552
+ | `api_base` | GitHub API base URL. Default: `https://api.github.com`. |
553
+ | `profiles_file` | Text file containing GitHub source profile/org URLs. Prefer top-level `sources_file` for mixed providers. |
554
+ | `profiles` | Inline list of GitHub source profile/org URLs. Prefer `[[sources]]` for mixed providers. |
555
+ | `tokens` | GitHub source token entries. These are also attached to GitHub URLs read from `sources_file`. |
556
+
557
+ Source options:
558
+
559
+ | Key | Meaning |
560
+ | --- | --- |
561
+ | `url` | Source profile, org, group, namespace, or workspace URL. |
562
+ | `platform` | `github`, `gitlab`, `forgejo`, `sourcehut`, or `bitbucket`. Usually inferred from `url`. |
563
+ | `api_base` | Optional API override for self-hosted or enterprise instances. |
564
+ | `owner` | Optional owner/namespace/workspace override. |
565
+ | `tokens` | Source API and HTTPS clone tokens. Use `env` instead of hardcoding secrets. |
566
+ | `watch` | Enable or disable this source in watching mode. Default: `true`. |
567
+ | `watch_interval_seconds` | Per-source polling interval override. |
568
+ | `watch_action` | Per-source action override: `full`, `local`, or `remote`. |
569
+
570
+ Watch options:
571
+
572
+ | Key | Meaning |
573
+ | --- | --- |
574
+ | `interval_seconds` | Default polling interval for sources in watching mode. |
575
+ | `action` | Default action for changed repositories: `full`, `local`, or `remote`. |
576
+ | `initial_run` | Process repositories the first time they are seen. If `false`, AGMH records current fingerprints and waits for later changes. |
577
+ | `once` | Run one polling cycle and exit. Mainly useful for tests, cron-like runs, or supervised debugging. |
578
+
579
+ Backup options:
580
+
581
+ | Key | Meaning |
582
+ | --- | --- |
583
+ | `local_dir` | Local mirror storage directory. |
584
+ | `clone_protocol` | `https` or `ssh` for source clone URLs. |
585
+ | `include_archived` | Include archived repositories. |
586
+ | `include_forks` | Include forked repositories. |
587
+ | `include_private_for_authenticated_user` | When the token belongs to the source user, include private repositories. |
588
+ | `lfs` | Run `git lfs fetch --all` after mirror updates. |
589
+ | `marker_enabled` | Write a provenance marker commit before remote mirrors. Default: `true`. Set to `false` to avoid modifying mirrored repositories. |
590
+ | `marker_filename` | Marker file name. Default: `agmh.txt`. |
591
+ | `push_mode` | `mirror`, `portable-mirror`, `all`, or `default`. |
592
+
593
+ Retry options:
594
+
595
+ | Key | Meaning |
596
+ | --- | --- |
597
+ | `max_retries` | Maximum retry attempts for transient API and Git network failures. |
598
+ | `base_delay_seconds` | Initial retry delay. |
599
+ | `max_delay_seconds` | Maximum exponential backoff delay. |
600
+ | `request_timeout_seconds` | API request timeout. |
601
+ | `rate_limit_sleep_seconds` | Sleep interval when rate limited and no reset time is available. |
602
+ | `wait_on_rate_limit` | Wait and resume instead of failing on rate limits. |
603
+
604
+ Git options:
605
+
606
+ | Key | Meaning |
607
+ | --- | --- |
608
+ | `author_name` | Git author name for marker commits. |
609
+ | `author_email` | Git author email for marker commits. |
610
+ | `commit_message` | Commit message for the marker commit. Supports `{version}`. Default: `Backuping with AGMH v{version}`. |
611
+ | `ssh_identity_file` | Private key for Git SSH operations. |
612
+ | `ssh_command` | Full `GIT_SSH_COMMAND` override. |
613
+ | `ssh_identities_only` | Add `-o IdentitiesOnly=yes` when using `ssh_identity_file`. |
614
+ | `ssh_batch_mode` | Add `-o BatchMode=yes`, useful for non-interactive jobs. |
615
+ | `ssh_strict_host_key_checking` | `yes`, `no`, or `accept-new`. |
616
+
617
+ Destination options:
618
+
619
+ | Key | Meaning |
620
+ | --- | --- |
621
+ | `url` | Destination account, group, org, or namespace URL. |
622
+ | `platform` | `github`, `gitlab`, `forgejo`, `sourcehut`, or `bitbucket`. |
623
+ | `api_base` | Optional API override for self-hosted instances. |
624
+ | `owner` | Optional owner/namespace override. |
625
+ | `tokens` | Destination API/Git tokens. |
626
+ | `visibility` | `mirror`, `public`, `private`, or `unlisted`. |
627
+ | `push_mode` | Override push mode for that destination. |
628
+ | `create` | Create repositories through the destination API. |
629
+ | `allow_existing` | Treat existing repositories as usable. |
630
+ | `git_username` | Username for HTTPS Git push URLs. |
631
+ | `push_url_template` | Custom push URL, for example SourceHut SSH. |
632
+
633
+ Notification options:
634
+
635
+ | Key | Meaning |
636
+ | --- | --- |
637
+ | `enabled` | Enable webhook notifications. Default: `false`. |
638
+ | `events` | Global event filter. Use `["*"]` for all enabled events. |
639
+ | `fail_silently` | Log webhook delivery errors instead of failing the workflow. Default: `true`. |
640
+ | `timeout_seconds` | HTTP timeout for webhook delivery. |
641
+
642
+ Supported notification events:
643
+
644
+ | Event | When it fires |
645
+ | --- | --- |
646
+ | `start` | Workflow starts, with a sanitized config snapshot. |
647
+ | `finish` | Workflow finishes, with exit code. |
648
+ | `local_saved` | A repository mirror was cloned or updated locally. |
649
+ | `remote_saved` | A repository was pushed to a destination. |
650
+ | `watch_check` | Watching mode starts checking a source for updates. |
651
+ | `watch_update` | Watching mode found a changed or first-seen repository and includes the next action. |
652
+ | `watch_none` | Watching mode found no updates and includes the next polling interval. |
653
+ | `error` | A source discovery, clone, marker, create, push, or workflow error happened. |
654
+
655
+ Webhook options:
656
+
657
+ | Key | Meaning |
658
+ | --- | --- |
659
+ | `name` | Human-readable webhook name used in local warnings. |
660
+ | `platform` | `generic`, `discord`, or `telegram`. |
661
+ | `enabled` | Per-webhook enable switch. Default: `true`. |
662
+ | `events` | Per-webhook event filter. Use `["*"]` for all events allowed globally. |
663
+ | `url` / `url_env` | Generic or Discord webhook URL. Prefer `url_env`. |
664
+ | `headers` | Extra headers for generic webhooks. |
665
+ | `username` | Discord webhook username override. |
666
+ | `avatar_url` | Discord webhook avatar override. |
667
+ | `thread_id` | Discord forum/thread selector query parameter. |
668
+ | `bot_token` / `bot_token_env` | Telegram bot token. Prefer `bot_token_env`. |
669
+ | `chat_id` / `chat_id_env` | Telegram chat ID. |
670
+ | `api_base` | Telegram API base URL. Default: `https://api.telegram.org`. |
671
+ | `parse_mode` | Telegram parse mode, for example `HTML`. |
672
+ | `message_thread_id` | Telegram forum topic ID. |
673
+ | `disable_web_page_preview` | Telegram link preview switch. Default: `true`. |
674
+
675
+ Example webhooks:
676
+
677
+ ```toml
678
+ [notifications]
679
+ enabled = true
680
+ events = ["*"]
681
+ fail_silently = true
682
+
683
+ [[webhooks]]
684
+ name = "ops-discord"
685
+ platform = "discord"
686
+ url_env = "DISCORD_WEBHOOK_URL"
687
+ events = ["start", "finish", "error", "local_saved", "remote_saved", "watch_update"]
688
+ username = "AGMH"
689
+
690
+ [[webhooks]]
691
+ name = "ops-telegram"
692
+ platform = "telegram"
693
+ bot_token_env = "TELEGRAM_BOT_TOKEN"
694
+ chat_id_env = "TELEGRAM_CHAT_ID"
695
+ events = ["error", "watch_update", "watch_none"]
696
+ parse_mode = "HTML"
697
+
698
+ [[webhooks]]
699
+ name = "ops-generic"
700
+ platform = "generic"
701
+ url_env = "AGMH_WEBHOOK_URL"
702
+ events = ["*"]
703
+ ```
704
+
705
+ Webhook notifications never include token values, webhook URLs, Telegram bot
706
+ tokens, or destination push URLs containing credentials. The `start` event
707
+ includes sources, destinations, modes, counts, and other operational settings
708
+ from a sanitized config snapshot.
709
+
710
+ ### Tokens
711
+
712
+ Use environment variables. Do not hardcode tokens into config files committed to
713
+ Git.
714
+
715
+ GitHub:
716
+
717
+ ```bash
718
+ export GITHUB_TOKEN="..."
719
+ ```
720
+
721
+ GitHub destination:
722
+
723
+ ```bash
724
+ export GITHUB_DEST_TOKEN="..."
725
+ ```
726
+
727
+ GitLab:
728
+
729
+ ```bash
730
+ export GITLAB_TOKEN="..."
731
+ ```
732
+
733
+ Codeberg:
734
+
735
+ ```bash
736
+ export CODEBERG_TOKEN="..."
737
+ ```
738
+
739
+ SourceHut:
740
+
741
+ ```bash
742
+ export SOURCEHUT_TOKEN="..."
743
+ ```
744
+
745
+ Webhooks:
746
+
747
+ ```bash
748
+ export DISCORD_WEBHOOK_URL="..."
749
+ export TELEGRAM_BOT_TOKEN="..."
750
+ export TELEGRAM_CHAT_ID="..."
751
+ export AGMH_WEBHOOK_URL="..."
752
+ ```
753
+
754
+ You can pass extra tokens from the CLI:
755
+
756
+ ```bash
757
+ agmh run \
758
+ --source https://github.com/haltman-io/ \
759
+ --github-token env:GITHUB_TOKEN \
760
+ --source-token gitlab:env:GITLAB_SOURCE_TOKEN \
761
+ --destination https://gitlab.com/haltman-io \
762
+ --destination-token gitlab:env:GITLAB_TOKEN
763
+ ```
764
+
765
+ Use `--github-token` as a shortcut for GitHub sources. Use `--source-token platform:...`
766
+ for other source providers, for example `gitlab:env:GITLAB_TOKEN`,
767
+ `forgejo:env:CODEBERG_TOKEN`, `bitbucket:env:BITBUCKET_TOKEN`,
768
+ `bitbucket:you@example.com:env:BITBUCKET_TOKEN`, or `sourcehut:env:SOURCEHUT_TOKEN`.
769
+
770
+ Multiple tokens are allowed. AGMH rotates tokens when a token is rejected, rate
771
+ limited, or temporarily unusable.
772
+
773
+ In TOML arrays, every token entry must be separated by a comma:
774
+
775
+ ```toml
776
+ [github]
777
+ tokens = [
778
+ { env = "GITHUB_TOKEN", name = "github-primary" },
779
+ { env = "GITHUB_TOKEN_2", name = "github-secondary" },
780
+ ]
781
+ ```
782
+
783
+ You can also use a named token table:
784
+
785
+ ```toml
786
+ [github.tokens]
787
+ github-primary = { env = "GITHUB_TOKEN" }
788
+ github-secondary = "env:GITHUB_TOKEN_2"
789
+ ```
790
+
791
+ ## Operations
792
+
793
+ ### Marker File
794
+
795
+ By default, before pushing to destinations, AGMH writes a marker file into the
796
+ default branch of the local mirror:
797
+
798
+ ```txt
799
+ agmh.txt
800
+ ```
801
+
802
+ The marker contains:
803
+
804
+ ```txt
805
+ source_url=https://github.com/owner/repo
806
+ downloaded_at=2026-06-12T00:00:00Z
807
+ marker_created_at=2026-06-12T00:00:01Z
808
+ ```
809
+
810
+ This is intentional. It makes it clear where the backup came from and when the
811
+ backup process created the provenance marker.
812
+
813
+ Disable this repository modification with:
814
+
815
+ ```toml
816
+ [backup]
817
+ marker_enabled = false
818
+ ```
819
+
820
+ When `marker_enabled` is `false`, AGMH does not create or update the marker file
821
+ and does not create a marker commit before pushing to destinations.
822
+
823
+ ### Push Modes
824
+
825
+ `mirror`:
826
+
827
+ Uses `git push --mirror` when the destination accepts every ref.
828
+
829
+ `portable-mirror`:
830
+
831
+ Pushes branches and tags while excluding platform-specific refs such as
832
+ GitHub pull request refs under `refs/pull/*`. This is useful for Codeberg and
833
+ Forgejo, which can reject hidden refs.
834
+
835
+ `all`:
836
+
837
+ Runs `git push --all` and then `git push --tags`.
838
+
839
+ `default`:
840
+
841
+ Pushes only the default branch.
842
+
843
+ Recommended defaults:
844
+
845
+ | Destination | Recommended push mode |
846
+ | --- | --- |
847
+ | GitHub | `mirror`, automatically translated to `portable-mirror` |
848
+ | GitLab | `mirror` |
849
+ | Codeberg/Forgejo | `mirror`, automatically translated to `portable-mirror` |
850
+ | SourceHut | `mirror` over SSH, or `portable-mirror` if hidden refs cause rejection |
851
+ | Bitbucket | `portable-mirror` |
852
+
853
+ ### Proxy Usage
854
+
855
+ Use an HTTP or HTTPS proxy:
856
+
857
+ ```bash
858
+ agmh run --config agmh.config.toml --proxy http://127.0.0.1:8080 --verbose
859
+ ```
860
+
861
+ Use a remote proxy:
862
+
863
+ ```bash
864
+ agmh run --config agmh.config.toml --proxy http://83.143.242.45:31343 --verbose
865
+ ```
866
+
867
+ If the proxy intercepts TLS and you know what you are doing:
868
+
869
+ ```bash
870
+ agmh run --config agmh.config.toml --proxy http://127.0.0.1:8080 --insecure --verbose
871
+ ```
872
+
873
+ `--insecure` is equivalent to `-k`:
874
+
875
+ ```bash
876
+ agmh run --config agmh.config.toml --proxy http://127.0.0.1:8080 -k
877
+ ```
878
+
879
+ This disables certificate verification for API calls and sets
880
+ `GIT_SSL_NO_VERIFY=true` for Git HTTPS operations.
881
+
882
+ #### Segfault.net Proxy Route
883
+
884
+ When GitHub API access is being rate-limited, blocked, or degraded from your
885
+ local network, you can route AGMH through a temporary HTTP proxy exposed from a
886
+ Segfault.net disposable root server.
887
+
888
+ Segfault.net is a project from The Hacker's Choice (THC). THC is an international hacker and IT security research group founded in 1995.
889
+ Segfault.net provides disposable root servers: each SSH login creates a root
890
+ server inside a virtual machine, with a public reverse TCP/UDP port option and
891
+ outbound traffic routed through upstream VPN networks. This makes it useful as a
892
+ temporary network exit path when the GitHub API is unusable from your current
893
+ IP address.
894
+
895
+ Open a Segfault.net shell:
896
+
897
+ ```bash
898
+ ssh root@segfault.net
899
+ ```
900
+
901
+ The public demo password is:
902
+
903
+ ```text
904
+ segfault
905
+ ```
906
+
907
+ Inside the Segfault.net server, request a public reverse port:
908
+
909
+ ```bash
910
+ curl sf/port
911
+ ```
912
+
913
+ Example output:
914
+
915
+ ```text
916
+ Tip: Type cat /config/self/reverse_* for details.
917
+ Tip: Type rshell to start listening.
918
+ Tip: Type curl sf/port to assign a new port.
919
+ Your reverse Port is 83.143.242.45 31343 [83.143.242.45:31343]
920
+ ```
921
+
922
+ Start an HTTP proxy listener on that assigned port:
923
+
924
+ ```bash
925
+ gost -L http://:31343
926
+ ```
927
+
928
+ Keep this SSH session open. The proxy exists only while the Segfault.net
929
+ environment and the `gost` process are alive. If the shell is closed or a new
930
+ Segfault.net server is created, request a new port and update the AGMH command.
931
+
932
+ On your workstation, run AGMH through the public proxy:
933
+
934
+ ```bash
935
+ agmh run \
936
+ --config agmh.config.toml \
937
+ --verbose \
938
+ --proxy http://83.143.242.45:31343 \
939
+ --insecure
940
+ ```
941
+
942
+ Use `--insecure` or `-k` only when TLS verification fails because of the proxy
943
+ path or interception layer. If normal certificate validation works through the
944
+ proxy, remove `--insecure`.
945
+
946
+ Before running a full migration, test the proxy path directly:
947
+
948
+ ```bash
949
+ curl -I \
950
+ --proxy http://83.143.242.45:31343 \
951
+ https://api.github.com/users/extencil
952
+ ```
953
+
954
+ Then run a short AGMH dry run with retries disabled:
955
+
956
+ ```bash
957
+ agmh run \
958
+ --config agmh.config.toml \
959
+ --dry-run \
960
+ --verbose \
961
+ --proxy http://83.143.242.45:31343 \
962
+ --insecure \
963
+ --request-timeout 5 \
964
+ --max-retries 0
965
+ ```
966
+
967
+ This proxy path affects AGMH API calls and Git HTTPS operations. It does not
968
+ automatically proxy SSH pushes such as `git@git.sr.ht:~user/repo`, because SSH
969
+ does not use the HTTP proxy settings.
970
+
971
+ ### SSH Usage
972
+
973
+ SourceHut is best used with SSH for Git pushes.
974
+
975
+ SourceHut destination:
976
+
977
+ ```toml
978
+ [[destinations]]
979
+ url = "https://git.sr.ht/~extencil"
980
+ platform = "sourcehut"
981
+ tokens = [{ env = "SOURCEHUT_TOKEN", name = "sourcehut-primary" }]
982
+ visibility = "mirror"
983
+ push_mode = "mirror"
984
+ push_url_template = "git@git.sr.ht:~{owner}/{repo}"
985
+ ```
986
+
987
+ If your SSH key is not a default key, configure it:
988
+
989
+ ```toml
990
+ [git]
991
+ ssh_identity_file = "/home/user/.ssh/sourcehut_ed25519"
992
+ ssh_identities_only = true
993
+ ssh_strict_host_key_checking = "accept-new"
994
+ ```
995
+
996
+ Or pass it at runtime:
997
+
998
+ ```bash
999
+ agmh run --config agmh.config.toml \
1000
+ --ssh-key ~/.ssh/sourcehut_ed25519 \
1001
+ --ssh-strict-host-key-checking accept-new
1002
+ ```
1003
+
1004
+ If the key has a passphrase, use `ssh-agent`:
1005
+
1006
+ ```bash
1007
+ eval "$(ssh-agent -s)"
1008
+ ssh-add ~/.ssh/sourcehut_ed25519
1009
+ ```
1010
+
1011
+ Test before running AGMH:
1012
+
1013
+ ```bash
1014
+ ssh -T -i ~/.ssh/sourcehut_ed25519 -o IdentitiesOnly=yes git@git.sr.ht
1015
+ ```
1016
+
1017
+ Do not use a private key directly from a Windows-mounted path such as
1018
+ `/mnt/c/Users/...` if OpenSSH reports permissive permissions. Copy it into the
1019
+ Linux filesystem and lock permissions:
1020
+
1021
+ ```bash
1022
+ mkdir -p ~/.ssh
1023
+ cp /mnt/c/Users/andre/.ssh/private_id_ed25519 ~/.ssh/sourcehut_ed25519
1024
+ chmod 700 ~/.ssh
1025
+ chmod 600 ~/.ssh/sourcehut_ed25519
1026
+ ```
1027
+
1028
+ ### Dry Run
1029
+
1030
+ Dry-run still calls platform APIs for discovery. It does not create repos,
1031
+ clone, add marker commits, or push.
1032
+
1033
+ ```bash
1034
+ agmh run --config agmh.config.toml --dry-run --verbose
1035
+ ```
1036
+
1037
+ Fail fast while debugging network/proxy issues:
1038
+
1039
+ ```bash
1040
+ agmh run --config agmh.config.toml \
1041
+ --dry-run \
1042
+ --verbose \
1043
+ --request-timeout 5 \
1044
+ --max-retries 0
1045
+ ```
1046
+
1047
+ ### Resume and State
1048
+
1049
+ AGMH stores resumable state here:
1050
+
1051
+ ```txt
1052
+ .agmh/state.json
1053
+ ```
1054
+
1055
+ Logs are stored here:
1056
+
1057
+ ```txt
1058
+ .agmh/logs/
1059
+ ```
1060
+
1061
+ Show a state summary:
1062
+
1063
+ ```bash
1064
+ agmh state --config agmh.config.toml
1065
+ ```
1066
+
1067
+ Force completed steps to rerun:
1068
+
1069
+ ```bash
1070
+ agmh run --config agmh.config.toml --force
1071
+ ```
1072
+
1073
+ Ignore existing state:
1074
+
1075
+ ```bash
1076
+ agmh run --config agmh.config.toml --no-resume
1077
+ ```
1078
+
1079
+ ### Examples
1080
+
1081
+ Discover only:
1082
+
1083
+ ```bash
1084
+ agmh discover --sources sources.txt
1085
+ ```
1086
+
1087
+ Write discovery output to JSON:
1088
+
1089
+ ```bash
1090
+ agmh discover --sources sources.txt --output discovered-repos.json
1091
+ ```
1092
+
1093
+ Back up one GitHub org to GitLab:
1094
+
1095
+ ```bash
1096
+ export GITHUB_TOKEN="..."
1097
+ export GITLAB_TOKEN="..."
1098
+
1099
+ agmh run \
1100
+ --source https://github.com/haltman-io/ \
1101
+ --destination https://gitlab.com/haltman-io \
1102
+ --destination-token gitlab:env:GITLAB_TOKEN \
1103
+ --github-token env:GITHUB_TOKEN \
1104
+ --verbose
1105
+ ```
1106
+
1107
+ Back up one GitLab group to GitHub:
1108
+
1109
+ ```bash
1110
+ export GITLAB_SOURCE_TOKEN="..."
1111
+ export GITHUB_DEST_TOKEN="..."
1112
+
1113
+ agmh run \
1114
+ --source https://gitlab.com/haltman-io/ \
1115
+ --source-token gitlab:env:GITLAB_SOURCE_TOKEN \
1116
+ --destination https://github.com/haltman-io-mirror \
1117
+ --destination-token github:env:GITHUB_DEST_TOKEN \
1118
+ --verbose
1119
+ ```
1120
+
1121
+ Back up one Codeberg account to GitLab:
1122
+
1123
+ ```bash
1124
+ export CODEBERG_SOURCE_TOKEN="..."
1125
+ export GITLAB_TOKEN="..."
1126
+
1127
+ agmh run \
1128
+ --source https://codeberg.org/haltman/ \
1129
+ --source-token forgejo:env:CODEBERG_SOURCE_TOKEN \
1130
+ --destination https://gitlab.com/haltman-codeberg-mirror \
1131
+ --destination-token gitlab:env:GITLAB_TOKEN \
1132
+ --verbose
1133
+ ```
1134
+
1135
+ Use Codeberg:
1136
+
1137
+ ```bash
1138
+ export CODEBERG_TOKEN="..."
1139
+
1140
+ agmh run \
1141
+ --source https://github.com/haltman-io/ \
1142
+ --destination https://codeberg.org/haltman \
1143
+ --destination-token forgejo:env:CODEBERG_TOKEN \
1144
+ --verbose
1145
+ ```
1146
+
1147
+ Use GitHub as a destination:
1148
+
1149
+ ```bash
1150
+ export GITHUB_TOKEN="..."
1151
+ export GITHUB_DEST_TOKEN="..."
1152
+
1153
+ agmh run \
1154
+ --source https://github.com/haltman-io/ \
1155
+ --destination https://github.com/haltman-io-mirror \
1156
+ --destination-token github:env:GITHUB_DEST_TOKEN \
1157
+ --github-token env:GITHUB_TOKEN \
1158
+ --verbose
1159
+ ```
1160
+
1161
+ Watch sources and mirror updates:
1162
+
1163
+ ```bash
1164
+ agmh watching \
1165
+ --config agmh.config.toml \
1166
+ --watch-interval 300 \
1167
+ --watch-action full \
1168
+ --verbose
1169
+ ```
1170
+
1171
+ Use SourceHut over SSH:
1172
+
1173
+ ```bash
1174
+ export SOURCEHUT_TOKEN="..."
1175
+
1176
+ agmh run \
1177
+ --source https://github.com/haltman-io/ \
1178
+ --destination https://git.sr.ht/~extencil \
1179
+ --destination-token sourcehut:env:SOURCEHUT_TOKEN \
1180
+ --ssh-key ~/.ssh/sourcehut_ed25519 \
1181
+ --ssh-strict-host-key-checking accept-new \
1182
+ --verbose
1183
+ ```
1184
+
1185
+ Use a proxy and ignore TLS validation:
1186
+
1187
+ ```bash
1188
+ agmh run \
1189
+ --config agmh.config.toml \
1190
+ --proxy http://127.0.0.1:8080 \
1191
+ --insecure \
1192
+ --verbose
1193
+ ```
1194
+
1195
+ ## Troubleshooting
1196
+
1197
+ ### `No module named anti_gh_ms_hysteria`
1198
+
1199
+ You are running from a source checkout without installing the package.
1200
+
1201
+ Fix:
1202
+
1203
+ ```bash
1204
+ python -m pip install -e ".[tui]"
1205
+ ```
1206
+
1207
+ Or run with:
1208
+
1209
+ ```bash
1210
+ PYTHONPATH=src python3 -m anti_gh_ms_hysteria run --help
1211
+ ```
1212
+
1213
+ ### It hangs on source discovery
1214
+
1215
+ The first source API request is waiting on network, DNS, proxy, or TLS.
1216
+
1217
+ Run:
1218
+
1219
+ ```bash
1220
+ agmh run --config agmh.config.toml --dry-run -v --request-timeout 5 --max-retries 0
1221
+ ```
1222
+
1223
+ Check the source API directly. For GitHub:
1224
+
1225
+ ```bash
1226
+ curl -I --max-time 10 https://api.github.com/users/extencil
1227
+ ```
1228
+
1229
+ ### Proxy returns HTTP 500
1230
+
1231
+ Use verbose mode. AGMH prints full HTTP response details for failed HTTP
1232
+ responses:
1233
+
1234
+ ```bash
1235
+ agmh run --config agmh.config.toml --proxy http://127.0.0.1:8080 --insecure --verbose --max-retries 0
1236
+ ```
1237
+
1238
+ If the response body is from the proxy, fix the proxy. If it is from the
1239
+ destination, inspect the API response.
1240
+
1241
+ ### GitLab rejects `.github`
1242
+
1243
+ GitLab does not allow project paths starting with a dot. AGMH maps `.github` to
1244
+ `dot-github` for GitLab destinations.
1245
+
1246
+ If old state points to the wrong path, AGMH rechecks create when destination
1247
+ path mapping changes.
1248
+
1249
+ ### Codeberg rejects `refs/pull/*`
1250
+
1251
+ Codeberg/Forgejo can reject hidden GitHub pull request refs:
1252
+
1253
+ ```txt
1254
+ deny updating a hidden ref
1255
+ ```
1256
+
1257
+ AGMH maps Forgejo/Codeberg `mirror` mode to `portable-mirror`, which pushes
1258
+ branches and tags without GitHub `refs/pull/*`.
1259
+
1260
+ ### `gnutls_handshake() failed`
1261
+
1262
+ This is usually a transient network, proxy, or TLS interruption during Git
1263
+ push. AGMH retries this class of Git network failure.
1264
+
1265
+ You can also reduce concurrency outside AGMH and rerun. Failed state entries are
1266
+ not marked `done`, so reruns continue from the failed push.
1267
+
1268
+ ### SourceHut says `Permission denied (publickey,keyboard-interactive)`
1269
+
1270
+ Your Git subprocess did not use the correct SSH key, or the key was rejected.
1271
+
1272
+ Test as the same user, without `sudo`:
1273
+
1274
+ ```bash
1275
+ ssh -T -i ~/.ssh/sourcehut_ed25519 -o IdentitiesOnly=yes git@git.sr.ht
1276
+ ```
1277
+
1278
+ Then run:
1279
+
1280
+ ```bash
1281
+ agmh run --config agmh.config.toml --ssh-key ~/.ssh/sourcehut_ed25519
1282
+ ```
1283
+
1284
+ ### OpenSSH says `UNPROTECTED PRIVATE KEY FILE`
1285
+
1286
+ Private key permissions are too open. Fix:
1287
+
1288
+ ```bash
1289
+ chmod 700 ~/.ssh
1290
+ chmod 600 ~/.ssh/sourcehut_ed25519
1291
+ ```
1292
+
1293
+ If the key is under `/mnt/c`, copy it to the Linux filesystem first.
1294
+
1295
+ ### `HTTP 401`
1296
+
1297
+ The token is invalid, expired, missing required scopes, or belongs to the wrong
1298
+ account.
1299
+
1300
+ Check the environment:
1301
+
1302
+ ```bash
1303
+ printenv GITHUB_TOKEN
1304
+ printenv GITHUB_DEST_TOKEN
1305
+ printenv GITLAB_TOKEN
1306
+ printenv CODEBERG_TOKEN
1307
+ printenv SOURCEHUT_TOKEN
1308
+ printenv DISCORD_WEBHOOK_URL
1309
+ printenv TELEGRAM_BOT_TOKEN
1310
+ printenv TELEGRAM_CHAT_ID
1311
+ ```
1312
+
1313
+ Do not paste tokens into logs or issues.
1314
+
1315
+ ### `HTTP 403` or `HTTP 429`
1316
+
1317
+ You hit rate limits or permission limits. AGMH rotates available tokens and, if
1318
+ configured, waits for the reset window.
1319
+
1320
+ Use more tokens:
1321
+
1322
+ ```toml
1323
+ [github]
1324
+ tokens = [
1325
+ { env = "GITHUB_TOKEN", name = "github-primary" },
1326
+ { env = "GITHUB_TOKEN_2", name = "github-secondary" },
1327
+ ]
1328
+ ```
1329
+
1330
+ ### Repository already exists
1331
+
1332
+ By default, `allow_existing = true` lets AGMH continue and push into an existing
1333
+ destination repository.
1334
+
1335
+ ### Existing state skips something you want to retry
1336
+
1337
+ Use:
1338
+
1339
+ ```bash
1340
+ agmh run --config agmh.config.toml --force
1341
+ ```
1342
+
1343
+ or edit `.agmh/state.json` carefully.
1344
+
1345
+ ## Security Notes
1346
+
1347
+ - Prefer environment variables for secrets.
1348
+ - Do not commit `.agmh/`, `backups/`, `agmh.config.toml`, `sources.txt`, `destinations.txt`, or private config files.
1349
+ - Logs scrub configured token secrets.
1350
+ - If a token was ever printed before scrubbing existed, rotate it.
1351
+ - `--insecure` is useful for debugging or intercepting proxies, but it disables TLS verification.
1352
+ - SSH private keys must be readable only by your user.
1353
+
1354
+ ## Repository Layout
1355
+
1356
+ ```txt
1357
+ src/anti_gh_ms_hysteria/
1358
+ cli.py CLI entrypoint
1359
+ runner.py workflow orchestration
1360
+ config.py TOML and CLI config loading
1361
+ git_ops.py clone, marker commit, push operations
1362
+ http.py API client, retries, proxy, TLS handling
1363
+ state.py resumable state file
1364
+ sources/ GitHub, GitLab, Forgejo, Bitbucket, SourceHut discovery adapters
1365
+ destinations/ GitHub, GitLab, Forgejo, Bitbucket, SourceHut adapters
1366
+ tests/
1367
+ test_config_and_utils.py
1368
+ .github/workflows/
1369
+ ci.yml tests and package checks
1370
+ release-please.yml release PR, changelog, tag, GitHub Release, PyPI publish
1371
+ publish-pypi.yml manual PyPI publish fallback through Trusted Publishing
1372
+ publish-testpypi.yml manual TestPyPI publish through Trusted Publishing
1373
+ pyproject.toml package metadata and build configuration
1374
+ MANIFEST.in source distribution file list
1375
+ release-please-config.json
1376
+ .release-please-manifest.json
1377
+ ```
1378
+
1379
+ ## Development
1380
+
1381
+ Install:
1382
+
1383
+ ```bash
1384
+ python -m pip install -e ".[tui]"
1385
+ ```
1386
+
1387
+ Run tests:
1388
+
1389
+ ```bash
1390
+ PYTHONPATH=src python -m unittest discover -s tests -v
1391
+ ```
1392
+
1393
+ Compile check:
1394
+
1395
+ ```bash
1396
+ PYTHONPATH=src python -m compileall -q src tests
1397
+ ```
1398
+
1399
+ CLI smoke test:
1400
+
1401
+ ```bash
1402
+ PYTHONPATH=src python -m anti_gh_ms_hysteria run --help
1403
+ ```
1404
+
1405
+ Package check:
1406
+
1407
+ ```bash
1408
+ python -m build
1409
+ python -m twine check --strict dist/*
1410
+ ```
1411
+
1412
+ ## Project Background
1413
+
1414
+ ### Risk Model
1415
+
1416
+ This tool exists because important work should not depend on a single platform
1417
+ remaining available, cooperative, or operational forever.
1418
+
1419
+ AGMH was built after past platform access incidents made us reassess the risk of
1420
+ being locked out of a large technology platform without enough time to preserve
1421
+ our work or coordinate with the people closest to a project. The risk is similar
1422
+ to an abrupt offboarding process where access is disabled so quickly that a
1423
+ person cannot even send a final email to close colleagues.
1424
+
1425
+ For software projects, that access risk is broader than any single account or
1426
+ provider. A team can lose continuity because of enforcement actions, sanctions,
1427
+ provider policy changes, operational outages, service degradation, acquisition
1428
+ risk, or a platform eventually disappearing. The critical issue is
1429
+ centralization: if repository history, issues, branches, tags, release metadata,
1430
+ and collaboration context all live in one place, a disruption in that place can
1431
+ have a large impact on the surrounding ecosystem.
1432
+
1433
+ This is not a personal fight with GitHub. It is a risk-management and business
1434
+ continuity problem. AGMH provides a practical way to keep local mirrors, move
1435
+ repositories between forges, preserve Git history, and maintain high
1436
+ availability of project information when a centralized platform becomes
1437
+ unavailable or unsuitable.
1438
+
1439
+ AGMH is produced by **Haltman.IO** and released freely so others can protect
1440
+ their own work.
1441
+
1442
+ This tool has already been used to back up repositories from `@extencil` and
1443
+ `@haltman-io` to GitLab, Codeberg, and SourceHut successfully.
1444
+
1445
+ ### Continuity Incident Timeline
1446
+
1447
+ AGMH was used to move the work of `@extencil` and `@haltman-io` away from a
1448
+ single forge dependency and into independent mirrors:
1449
+
1450
+ - GitLab: https://gitlab.com/extencil
1451
+ - Codeberg: https://codeberg.org/extencil
1452
+ - SourceHut: https://git.sr.ht/~extencil
1453
+ - GitLab: https://gitlab.com/haltman-io
1454
+ - Codeberg: https://codeberg.org/haltman
1455
+
1456
+ The account access incident that reinforced this risk model followed this
1457
+ timeline:
1458
+
1459
+ | Event | Time |
1460
+ | --- | --- |
1461
+ | Account suspended/banned by platform enforcement | Monday, 2026-06-08, around 04:00 `America/Sao_Paulo` (`UTC-03:00`), approximately 2026-06-08 07:00 UTC |
1462
+ | Review ticket opened | 2026-06-08 07:59 UTC, 2026-06-08 04:59 `America/Sao_Paulo` |
1463
+ | Priority follow-up sent by our side | 2026-06-11 16:47 UTC, 2026-06-11 13:47 `America/Sao_Paulo` |
1464
+ | Case reviewed and reverted by GitHub | 2026-06-12 11:19 UTC, 2026-06-12 08:19 `America/Sao_Paulo` |
1465
+
1466
+ The incident would have been significantly more damaging without continuity
1467
+ procedures already in place. When Haltman.IO created its GitHub organization,
1468
+ other Haltman.IO members were assigned as organization owners. That avoided a
1469
+ complete lockout scenario.
1470
+
1471
+ Someone who is not part of an organization, or who is not an organization owner
1472
+ or repository administrator, cannot reliably operate that organization. They
1473
+ cannot recover organization-level access, manage owners and teams, change
1474
+ organization settings, manage repository permissions, configure secrets,
1475
+ webhooks, deploy keys, branch protection, or security settings, create or
1476
+ transfer repositories, publish releases, or consistently triage and merge work
1477
+ across the organization.
1478
+
1479
+ This matters because the affected work is operational, not cosmetic. Haltman.IO
1480
+ voluntarily sustains email-forwarding infrastructure associated with The
1481
+ Hacker's Choice, in collaboration with Phrack, Eurocompton, team-teso, Antisec,
1482
+ pwnbuffer, and other groups connected to cybersecurity research. A complete
1483
+ organization lockout would have affected the ability to manage the many
1484
+ repositories behind that email-forwarding stack.
1485
+
1486
+ That impact is not about minor product changes or visual polish. It affects the
1487
+ ability to coordinate proper vulnerability disclosure for people who self-host
1488
+ the email-forwarding stack, publish fixes, document operational changes, and
1489
+ credit researchers correctly when they report vulnerabilities.
1490
+
1491
+ It also affects our internal service expectations. There is no legal or
1492
+ commercial SLA: we do not sell this work, and the output is public work for the
1493
+ public. Still, we prefer to respond to issues and pull requests quickly. Acting
1494
+ like a large platform with effectively unbounded response times is neither our
1495
+ role nor consistent with Haltman.IO's operating values.
1496
+
1497
+ ### Haltman.IO
1498
+
1499
+ Haltman is a group of Brazilian hackers. Friends for over a decade, building
1500
+ public, privacy-first infrastructure and free software.
1501
+
1502
+ We build, break, audit, and publish.
1503
+
1504
+ We do not sell platforms. We do not run franchises.
1505
+
1506
+ We do not ask permission.
1507
+
1508
+ Haltman.IO links:
1509
+
1510
+ - Website: https://haltman.io/
1511
+ - Alternate website: https://haltman.org/
1512
+ - Contact: root@haltman.io, root@haltman.org
1513
+ - Join Haltman.IO: https://haltman.io/join/
1514
+ - Telegram group: https://t.me/haltman_group
1515
+
1516
+ ### Operating Values
1517
+
1518
+ | Doctrine | Value |
1519
+ | --- | --- |
1520
+ | 01 Independence | We answer to no one. No board. No investors. No sponsors. Our independence guarantees our freedom. |
1521
+ | 02 Transparency | Every tool is open source. Every decision is visible. No back rooms. No hidden agendas. |
1522
+ | 03 Public Output | We publish. We document. We release. Our work speaks for itself. Not our marketing. |
1523
+ | 04 No Hierarchy | Flat structure. No leaders. No bosses. No titles. No org charts. Respect is earned by output. |
1524
+ | 05 Mutual Aid | When one of us needs help, the others show up. No invoices. No politics. Just engineering. |
1525
+ | 06 No Compromise | We do not water down our principles for comfort, profit, or acceptance. Those who trade freedom for security end up with neither. |
1526
+
1527
+ ## Project Files
1528
+
1529
+ - [CHANGELOG.md](CHANGELOG.md): release notes and pending changes.
1530
+ - [SECURITY.md](SECURITY.md): private vulnerability reporting policy.
1531
+ - [CONTRIBUTING.md](CONTRIBUTING.md): development setup and contribution checks.
1532
+ - [RELEASING.md](RELEASING.md): Release Please and PyPI publishing process.
1533
+ - [SUPPORT.md](SUPPORT.md): support paths for bugs, questions, and security reports.
1534
+ - [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md): collaboration expectations.
1535
+ - [MAINTAINERS.md](MAINTAINERS.md): maintainer and security contact information.
1536
+
1537
+ ## References
1538
+
1539
+ - Unlicense: https://unlicense.org/
1540
+ - GitHub personal access tokens: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens
1541
+ - GitHub repositories REST API: https://docs.github.com/en/rest/repos/repos
1542
+ - GitHub REST API rate limits: https://docs.github.com/rest/rate-limit/rate-limit
1543
+ - GitLab personal access tokens: https://docs.gitlab.com/user/profile/personal_access_tokens/
1544
+ - GitLab Projects API: https://docs.gitlab.com/api/projects/
1545
+ - GitLab Groups API: https://docs.gitlab.com/api/groups/
1546
+ - Codeberg access tokens: https://docs.codeberg.org/advanced/access-token/
1547
+ - Forgejo API usage: https://forgejo.org/docs/latest/user/api-usage/
1548
+ - Bitbucket repositories API: https://developer.atlassian.com/cloud/bitbucket/rest/api-group-repositories/
1549
+ - SourceHut git.sr.ht GraphQL API docs: https://docs.sourcehut.org/git.sr.ht/
1550
+ - SourceHut GraphQL API docs: https://docs.sourcehut.org/
1551
+ - SourceHut: https://sourcehut.org/
1552
+ - Discord Webhook Resource: https://docs.discord.com/developers/resources/webhook
1553
+ - Telegram Bot API: https://core.telegram.org/bots/api
1554
+ - The Hacker's Choice: https://www.thc.org/
1555
+ - Segfault.net disposable root servers: https://www.thc.org/segfault/
1556
+ - Segfault.net service notes: https://www.thc.org/segfault/free/
1557
+ - Segfault.net Server Centre source: https://github.com/hackerschoice/segfault
1558
+
1559
+ ## License
1560
+
1561
+ AGMH is released under the **Unlicense**.
1562
+
1563
+ This means the project is dedicated to the public domain to the fullest extent
1564
+ possible. See:
1565
+
1566
+ - https://unlicense.org/
1567
+ - [LICENSE](LICENSE)
1568
+
1569
+ ## Author
1570
+
1571
+ Author: **extencil** <extencil@segfault.net>
1572
+
1573
+ Repository: [haltman-io/agmh](https://github.com/haltman-io/agmh)
1574
+
1575
+ Produced by **Haltman.IO**.