This directory is the Python package imported by the gh-safe-repo launcher.
The repository root contains a thin launcher script named gh-safe-repo (no .py
extension). Its only job is to ensure the package is importable — by inserting the
repo root into sys.path when run directly — and then call gh_safe_repo.cli.main().
When the tool is installed via uv tool install . or pip install, the same
main() is wired up as the console-script entry point declared in pyproject.toml,
and the launcher is not used.
gh-safe-repo ← thin launcher (direct-run entry point)
gh_safe_repo/ ← this package (all real logic lives here)
| Module | Purpose |
|---|---|
cli.py |
main() — subparser dispatch to create, fix, scan commands |
commands/_common.py |
Shared helpers: CLIContext, parse_repo_arg(), build_context(), plan formatting, ANSI output |
commands/create.py |
create subcommand — new repo with safe defaults |
commands/fix.py |
fix subcommand — audit existing repo, show diff, apply corrections |
commands/scan.py |
scan subcommand — local-only secret scanning |
github_client.py |
Wrapper around gh api (subprocess); copy_repo(), push_local() |
config_manager.py |
INI config parsing via configparser; holds SAFE_DEFAULTS |
diff.py |
Change and Plan dataclasses; count_by_type() |
errors.py |
Custom exception hierarchy (GhSafeRepoError, etc.) |
security_scanner.py |
Pre-flight scanner: truffleHog dispatch, regex fallback, _unified_walk() |
plugins/base.py |
Abstract BasePlugin — defines the plan() / apply() interface |
plugins/repository.py |
Repo creation (POST /user/repos) and basic repo settings (PATCH) |
plugins/actions.py |
GitHub Actions permissions (allowed actions, workflow perms, SHA pinning) |
plugins/branch_protection.py |
Classic branch protection + Rulesets API |
plugins/security.py |
Dependabot alerts/security updates, secret scanning/push protection, private vuln reporting |
plugins/tag_protection.py |
Immutable tags via Rulesets API (prevent deletion/rewriting of release tags) |
templates/ |
File templates (currently empty) |
Each plugin follows a fetch → diff → apply cycle:
plan()— fetches current repo state from the GitHub API and compares it against the desired state from config. Returns aPlan(list ofChangeobjects tagged asADD/UPDATE/DELETE/SKIP).apply()— iterates the plan and makes only the API calls that correspond to real changes. No-ops (settings already at the desired value) produce no API calls.
This means audit mode and create mode share the same code path. The only difference is whether current state is fetched from an existing repo or assumed to be GitHub defaults.
cli.main()
│
├─ each plugin.plan() → Plan (list of Change)
├─ print plan table
└─ each plugin.apply() → API calls for non-SKIP changes
- Identify the GitHub API endpoint.
- Add the key and safe default to
config_manager.py:SAFE_DEFAULTS. - Add the corresponding entry (with a comment) to
gh-safe-repo.ini.examplein the repo root.test_config_consistency.pywill fail if the example file andSAFE_DEFAULTSdiverge. - Update the appropriate plugin's
plan()andapply()methods. - Add tests in
tests/test_plugins.py— allsubprocesscalls must be mocked.
- No runtime dependencies. Everything uses the Python standard library. Do not add third-party packages without prior discussion.
- All GitHub API calls go through
GitHubClient. Never callsubprocessorgh apidirectly from a plugin or fromcli.py. - Tokens are never logged. Debug output uses sanitised URLs;
GH_TOKENis injected into the child-process environment, not into logged command strings. fixemits repo identity in debug mode. Afterget_repo_data(),fixprints the repo'sid,full_name, andowner.typeto stderr when--debugis set, so users can confirm they are targeting the correct repo.GET /userandGET /repos/{owner}/{repo}are cached.GitHubClientcaches both; every plugin hits the cache instead of making a fresh HTTP call.