Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
gh-safe-repo — Python CLI that creates GitHub repos with safe defaults — branch protection, Dependabot, secret scanning, and pre-flight security scanning — applied automatically. | Kitploit
Tools/GitHubGitHub/ariesq/gh-safe-repo
General Purpose UtilitiesVulnerability ScannersScripting & AutomationConfiguration AuditingCloud SecurityDevSecOpsSecret Detection
GitHubariesq/gh-safe-repo

gh-safe-repo

Python CLI that creates GitHub repos with safe defaults — branch protection, Dependabot, secret scanning, and pre-flight security scanning — applied automatically.

View Repository
383520 days agoReviewed by Kitploit

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

gh-safe-repo

Create GitHub repositories with safe defaults applied automatically. Replaces the five-minute post-creation settings checklist with a single command.

root@kitploit:~
gh-safe-repo create <owner/repo>

Branch protection, immutable tags, Dependabot, restricted Actions permissions, secret scanning with push protection, and disabled wiki and projects — all configured before you write your first line of code.

gh-safe-repo is undergoing heavy development. It works well for the use-case of creating a new repo with secure defaults. I am working on polishing the CLI options to best align with users expectations. Expect breaking changes until we get to a point where I'm doing releases, and have CI/CD nailed down. ✌️


Table of Contents

  • Why
  • What It Changes
  • Requirements
  • Installation
  • Quick Start
  • CLI Reference
  • Dry Run / Plan Output
  • Fix Mode (Audit Existing Repos)
  • Mirroring Repos (--from)
  • Creating a Repo from a Local Directory (--local)
  • Pre-flight Security Scanner
  • Standalone scan
  • Suppressing false positives
  • Configuration
  • GitHub Plan Limitations
  • How It Works
  • Development

  • Why

    GitHub's default repository settings are optimised for discoverability and flexibility, not security. Every new repo ships with:

    • Wiki and Projects enabled (attack surface, even if unused)
    • Merge commits allowed (messy history, but not the main concern)
    • No branch protection (anyone with write access can push directly to main)
    • No Dependabot alerts
    • GitHub Actions with write permissions to the repository
    • Actions allowed to approve pull requests

    Fixing all of this manually takes minutes per repo and is easy to forget. gh-safe-repo applies an opinionated but practical set of defaults in one shot, with a plan preview so you know exactly what will change before anything does.


    What It Changes

    Repository settings

    SettingGitHub defaultSafe defaultNotes
    VisibilityPublicPrivatePass --public to override
    WikiEnabledDisabled
    ProjectsEnabledDisabled
    IssuesEnabledEnabled
    Delete branch on mergeOffOffSet to true in config for auto-cleanup
    Allow merge commitsOnOnSet to false in config for squash-only
    Allow squash mergeOnOn
    Allow rebase mergeOnOn

    GitHub Actions

    SettingGitHub defaultSafe default
    Allowed actionsAllSelected (GitHub-owned + verified creators; customisable)
    Default workflow permissionsRead/writeRead-only
    Actions can approve PRsYesNo
    Require SHA pinningNoYes (workflows must pin actions to a commit SHA, not a mutable tag)
    Fork PR approval policyFirst-time contributors new to GitHubAll external contributors — require approval before fork PR workflows run CI. Options: brand-new GitHub accounts only (GitHub default), first-time repo contributors, or all fork PRs (safest)

    Branch protection (public repos, or any repo on a paid plan)

    RuleValue
    Require pull request before mergeYes
    Required approving reviews1
    Dismiss stale reviews on pushYes
    Require conversation resolutionYes
    Allow force pushesNo
    Allow branch deletionNo
    Enforce on adminsNo (allows owner tooling to push)

    Branch protection is applied via the Rulesets API by default (use_rulesets = true): a single gh-safe-repo defaults ruleset covers every configured branch and expresses "admins can bypass" through a bypass actor rather than the classic enforce_admins flag. Set use_rulesets = false for the legacy classic per-branch path (kept for one release cycle).

    Migrating an existing repo from classic protection: if fix finds classic branch protection on a repo, it refuses to convert it to a ruleset unless you pass --migrate-branch-protection. Classic-only rules have no equivalent in the ruleset this tool builds and would be dropped silently otherwise — known gaps:

    • required_status_checks — required CI checks are not modelled in the ruleset body.
    • restrictions (push restrictions by user/team) — Rulesets model this differently via bypass actors; not a 1:1 map.
    • Per-branch divergence — a single shared-condition ruleset can't express different rules for master vs main.

    With the flag, fix creates/updates the ruleset and then deletes the classic protection on each branch so the two layers don't stack.

    Tag protection (public repos, or any repo on a paid plan)

    Tag protection creates a GitHub Ruleset targeting all tags (* by default, configurable via protected_tags). The following rules are enforced:

    Ruleset ruleEnforced?Notes
    Restrict creationsNo
    Restrict updatesYesPrevents rewriting / force-pushing tags
    Restrict deletionsYesPrevents git push --delete of tags
    Require linear historyNo
    Require deployments to succeedNo
    Require signed commitsNo
    Require status checks to passNo
    Block force pushesNo

    Repository admins are on the bypass list (consistent with the branch protection enforce_admins = false default). Only works on public repos or paid GitHub plans (same restriction as branch protection). Free-plan private repos will see this skipped in the plan output.

    Security

    FeatureBehaviour
    Dependabot alertsEnabled (public repos / paid plans)
    Dependabot security updatesEnabled (auto-opens PRs for vulnerable deps)
    Secret scanningAutomatic on public repos; enabled on private paid plans
    Push protectionEnabled (blocks commits containing supported secrets)
    Private vulnerability reportingEnabled (lets security researchers report privately)
    Dependency graphAutomatic on public repos; no REST API for private (UI only)

    Requirements

    • Python 3.8+
    • gh CLI installed and authenticated (gh auth login), or GITHUB_TOKEN set in your environment
    • For --local / --from (which push or clone code): your usual git credentials must be set up — either an SSH key loaded into ssh-agent (when gh config get git_protocol is ssh) or an HTTPS credential helper (gh auth setup-git configures one automatically). The OAuth token is not used for git push, so workflow files (.github/workflows/*) push without needing the OAuth workflow scope.
    • uv for installation from source (recommended)
    • truffleHog v3 (optional — used by the pre-flight scanner; auto-detected from PATH, or run via podman/docker; falls back to regex if neither is available)

    Installation

    From source with uv (recommended)

    root@kitploit:~
    git clone https://github.com/your-username/gh-safe-repo
    cd gh-safe-repo
    uv tool install .
    

    This installs gh-safe-repo into uv's tool environment and adds it to your PATH.

    Run directly without installing

    root@kitploit:~
    git clone https://github.com/your-username/gh-safe-repo
    cd gh-safe-repo
    uv sync           # creates .venv
    ./gh-safe-repo create <owner/repo>
    

    Verify

    root@kitploit:~
    gh-safe-repo --help
    

    Quick Start

    root@kitploit:~
    # Create a private repo with all safe defaults
    gh-safe-repo create <owner/repo>
    
    # Preview what would happen — no changes made
    gh-safe-repo create <owner/repo> --dry-run
    
    # Create a public repo (branch protection + security scanning applied)
    gh-safe-repo create <owner/repo> --public
    
    # Mirror an existing repo into a new private repo (with pre-flight scan)
    gh-safe-repo create <owner/repo> --from <owner/source>
    
    # Mirror a private repo to a new public repo (with pre-flight scan)
    gh-safe-repo create <owner/pub> --from <owner/priv> --public
    
    # Create a repo from a local directory (with pre-flight scan)
    gh-safe-repo create <owner/repo> --local ~/projects/myapp
    
    # Same, but make it public (branch protection applied before push)
    gh-safe-repo create <owner/repo> --local ~/projects/myapp --public
    
    # Audit an existing repo and apply any missing safe defaults
    gh-safe-repo fix <owner/repo>
    
    # Audit without making changes
    gh-safe-repo fix <owner/repo> --dry-run
    
    # Apply fixes without confirmation prompt (scripting/batch use)
    gh-safe-repo fix <owner/repo> --yes
    
    # Scan a local repo for secrets before pushing anywhere
    gh-safe-repo scan .
    gh-safe-repo scan ~/projects/myapp
    

    CLI Reference

    root@kitploit:~
    gh-safe-repo create <owner/repo> [OPTIONS]
    gh-safe-repo fix <owner/repo> [OPTIONS]
    gh-safe-repo scan <path> [OPTIONS]
    

    All commands that interact with GitHub require the owner/repo format (e.g. myuser/my-repo). For create, the owner is validated against your authenticated GitHub account to prevent mistakes on multi-account systems. For fix, admin permissions on the target repo are required instead, allowing you to fix repos owned by organizations or other accounts where you have admin access.

    create — Create a new repo

    OptionDescription
    --publicCreate as a public repo (default: private)
    --local PATHPush code from a local git repository into the new repo. Runs pre-flight scan first. Mutually exclusive with --from.
    --from OWNER/REPOMirror code from an existing repo into the new repo. Runs pre-flight scan. Mutually exclusive with --local.
    --yes / -ySkip confirmation prompt and apply immediately (for scripting/batch use)
    --dry-runPrint the plan without making any changes
    --jsonEmit the plan as JSON to stdout instead of the ANSI table
    --config [PATH]Path to config file; bare --config uses built-in defaults only
    --debugPrint every API call and response

    A plain create (no --local/--from) initializes the repo so a default branch exists for branch protection, then removes the auto-generated README.md so the new repo starts clean. Set auto_init = true in config to keep the README instead. --local/--from push your own history and never create a README.

    fix — Audit and fix an existing repo

    OptionDescription
    --yes / -ySkip confirmation prompt and apply immediately (for scripting/batch use)
    --dry-runShow settings diff without applying changes
    --jsonEmit the plan as JSON to stdout instead of the ANSI table
    --config [PATH]Path to config file; bare --config uses built-in defaults only
    --debugPrint every API call and response, plus resolved repo identity (id, full name, owner type)

    scan — Local secret scanning

    OptionDescription
    --config [PATH]Path to config file; bare --config uses built-in defaults only
    --debugShow scanner details

    Exit code is 0 if no critical findings, 1 if criticals are found.


    Dry Run / Plan Output

    --dry-run shows exactly what gh-safe-repo would do, without making any changes or API calls. Use it before running for real. Combine with --json for machine-readable plan output:

    root@kitploit:~
    gh-safe-repo create <owner/repo> --dry-run --json
    gh-safe-repo fix <owner/repo> --dry-run --json
    

    When --json is active, the plan is written to stdout as a JSON object and all other messages (progress, warnings, the "Dry run" footer) go to stderr, so the output is clean for piping or scripting.

    root@kitploit:~
    $ gh-safe-repo create <owner/repo> --dry-run
    
      Plan for my-project (private)
    
      Category            Action  Setting                          Value
      ──────────────────────────────────────────────────────────────────
      Repository          ADD     repository                       my-project (private)
      Repository          ADD     has_wiki                         false
      Repository          ADD     has_projects                     false
      Actions             ADD     default_workflow_permissions     read
      Actions             ADD     can_approve_pull_request_reviews false
      Branch Protection   SKIP    branch_protection                Not available for private repos on free plan
      Security            SKIP    dependabot_alerts                Not available for private repos on free plan
      1 setting skipped (GitHub plan limitation).
      Dry run — no changes made.
    

    Action colours:

    ActionMeaning
    ADD (green)New setting being applied
    UPDATE (yellow)Existing setting being changed (audit mode)
    DELETE (red)Setting being removed
    SKIP (dim)No action needed — already at the desired value, or feature unavailable on your plan/visibility combination

    JSON output (--json):

    root@kitploit:~
    {
      "changes": [
        { "type": "add",  "category": "repository",         "key": "has_wiki",  "old": null, "new": false, "reason": null },
        { "type": "skip", "category": "branch_protection",   "key": "branch_protection", "old": null, "new": null, "reason": "Not available for private repos on free plan" }
      ],
      "summary": { "add": 5, "skip": 2 }
    }
    

    summary only includes types that are present in the plan. Consumers should use .get("delete", 0) etc. rather than assuming all four keys are present.


    Fix Mode (Audit Existing Repos)

    fix compares an existing repo's current settings against the safe defaults and applies any corrections. No secret scanning — fix is purely about repo settings.

    root@kitploit:~
    # See what's out of compliance
    gh-safe-repo fix <owner/repo> --dry-run
    
    # Apply missing safe defaults
    gh-safe-repo fix <owner/repo>
    
    # Apply without confirmation prompt (scripting/batch use)
    gh-safe-repo fix <owner/repo> --yes
    

    Fix mode:

    1. Fetches the current value of every setting via the GitHub API
    2. Compares against desired safe defaults
    3. Shows a plan table with UPDATE for changed settings and SKIP for settings already at the desired value (no-op detection — it never makes API calls that would change nothing)
    4. Prompts for confirmation before applying (skip with --yes)

    Only real changes are applied — settings already at the desired value are shown as SKIP and generate no API calls.


    Mirroring Repos (--from)

    --from mirrors an existing repo into a new one with safe defaults. It works for both private and public destinations:

    root@kitploit:~
    # Mirror into a new private repo (default)
    gh-safe-repo create <owner/repo> --from <owner/source>
    
    # Mirror a private repo to a new public repo (riskiest operation — scanned thoroughly)
    gh-safe-repo create <owner/pub> --from <owner/priv> --public
    

    What happens, in order:

    1. Your git credentials for github.com are verified up front (SSH probe when gh config get git_protocol is ssh; HTTPS is trusted), so a missing key fails fast before any repo is created
    2. The source repo is cloned locally (full clone, no --depth, so truffleHog can walk the full commit history)
    3. The pre-flight security scanner runs on the local clone
    4. You review findings and confirm (or abort)
    5. A new repo is created (private by default, or public with --public)
    6. Actions permissions and security settings are applied (Dependabot, secret scanning, push protection)
    7. The full history is mirrored: git clone --mirror + git push --mirror
    8. Branch and tag protection are applied (after code push, so the target branch exists)

    If the scan reveals a problem and you abort, no code is ever copied to GitHub.

    Note: --from uses owner/repo format for both the source and destination.


    Creating a Repo from a Local Directory (--local)

    --local PATH is the local-to-GitHub counterpart to --from. It creates a new GitHub repo and pushes code from a local git repository. PATH must be an initialized git repository (git init or a clone).

    root@kitploit:~
    gh-safe-repo create <owner/repo> --local ~/projects/myapp
    gh-safe-repo create <owner/repo> --local ~/projects/myapp --public
    

    What happens, in order:

    1. Your git credentials for github.com are verified up front (SSH probe when gh config get git_protocol is ssh; HTTPS is trusted), so a missing key fails fast before any repo is created
    2. The pre-flight security scanner runs on the local directory directly (no clone needed)
    3. You review findings and confirm (or abort)
    4. A new repo is created, and actions permissions and security settings are applied
    5. The full history is pushed with push --all --tags (all branches and tags)
    6. Branch and tag protection are applied (after code push, so the target branch exists)
    7. origin is added to the original local repo pointing at the new GitHub URL, and the current branch's upstream tracking is configured — so git push and git pull work immediately without extra setup.

    Both --local and --from work for private and public repos. They are mutually exclusive.

    The local default branch (via git -C PATH symbolic-ref HEAD) is used to target branch protection rules, so protection lands on the right branch even if it isn't main.

    Tip: Run gh-safe-repo scan PATH first if you want to inspect findings without creating anything.


    Pre-flight Security Scanner

    The scanner runs locally and never sends code to GitHub. Use it standalone before any push, or it runs automatically as part of the --from and --local workflows.

    Standalone scan

    root@kitploit:~
    # Scan the current directory
    gh-safe-repo scan .
    
    # Scan an explicit path
    gh-safe-repo scan ~/projects/myapp
    

    Exit code is 0 if no critical findings, 1 if criticals are found — so it composes cleanly with other commands:

    root@kitploit:~
    gh-safe-repo scan . && git push
    

    The full [pre_flight_scan] config applies: banned_strings, max_file_size_mb, trufflehog_mode, etc.

    What it detects

    CategorySeverityExamples
    Hardcoded secretsCriticalAWS keys (AKIA…), GitHub tokens (ghp_…, github_pat_…), private keys, database URLs
    Banned stringsCriticalAny literal strings you configure (usernames, internal hostnames, codenames)
    AI context filesCriticalCLAUDE.md, AGENTS.md, .cursorrules, copilot-instructions.md, .cursor/ — may contain internal dev notes; git history may be more sensitive than the current version
    Email addressesWarningAny [email protected] pattern in working tree and git history
    Large filesWarningFiles over the configured size threshold (default: 100 MB)
    TODO/FIXME commentsInfo# TODO, # FIXME, # HACK, # XXX

    Scanner engine

    gh-safe-repo automatically picks the best available scanner using a three-step discovery chain:

    1. truffleHog v3 on PATH — runs trufflehog --version, verifies it is v3, and uses it. A v2 install or an unrecognised version prints a warning and falls through to step 2.
    2. podman or docker — if no native truffleHog is found, the scanner runs truffleHog in a container (ghcr.io/trufflesecurity/trufflehog:latest) using podman run or docker run, mounting the scan path read-only at the same absolute path so JSON output paths are identical to a native run.
    3. Regex fallback — if neither a native install nor a container runtime is available, a warning is printed and the regex scanner runs instead. It also always runs in addition to truffleHog for emails and TODOs, and catches lone key-ID patterns that truffleHog deliberately skips (truffleHog requires both halves of a credential pair, e.g. AWS Key ID and Secret Access Key, before flagging a finding).

    The selected scanner is shown in the "Running pre-flight security scan..." header and in the plan table's SCAN entry, e.g.:

    root@kitploit:~
    Running pre-flight security scan... (truffleHog v3.93.4)
    Running pre-flight security scan... (truffleHog via podman)
    Running pre-flight security scan... (regex only — see warning above)
    

    Environment variables respected by the container path: CONTAINER_RUNTIME to override runtime selection (e.g. CONTAINER_RUNTIME=docker), and TRUFFLEHOG_IMAGE to pin a specific image tag.

    Running truffleHog via podman or Docker (no local install)

    No manual setup is required. gh-safe-repo detects podman or docker automatically (step 2 above) and runs truffleHog in a container with the correct volume mounts. CONTAINER_RUNTIME and TRUFFLEHOG_IMAGE environment variables are respected.

    A shell wrapper (tools/trufflehog) and a Containerfile for building a pinned local image are provided in tools/ for users who want container-based truffleHog available system-wide, or who need an air-gapped image.

    Interactive review

    root@kitploit:~
    Pre-flight scan: my-private-project
    
      CRITICAL  my_private_project/config.py:12  AWS Access Key ID
                [redacted]
    
      WARNING   my_private_project/setup.py:3    Email address
                author_email="[email protected]"
    
      1 critical finding, 1 warning.
    
      Critical findings detected. Continue anyway? [y/N]:
    
    • Critical findings: Default is abort (N). You must explicitly type y to continue.
    • Warnings only: Default is continue (Y). Press Enter to proceed or type n to abort.
    • No findings: Scan completes silently and the workflow continues.

    Secrets are redacted in the output. Email addresses and TODOs show the matching line.

    Scan coverage

    Build-artifact directories (node_modules, __pycache__, .venv, venv, dist, build) are skipped by default to keep scans fast. In git repos, this skip is conditional: before pruning a directory, the scanner runs git ls-files -- <dir> to check whether any files inside are tracked. If they are, the directory is scanned normally.

    This means committed node_modules or dist trees — unusual, but they happen — are not silently missed. Uncommitted directories (the normal case) continue to be skipped as before.

    A warning is still printed when SKIP_DIRS subdirectories are found in a cloned source repo, since their presence may indicate that more content than expected is committed.

    Suppressing false positives

    Two config keys let you suppress known-safe findings without disabling entire check categories.

    scan_exclude_paths — skip files or directories entirely. Values are newline/comma-separated regex patterns matched against the relative file path. A matching file is excluded from every check: secrets, emails, TODOs, large files, and AI context file detection. The same patterns are also passed to truffleHog via --exclude-paths, so coverage is consistent regardless of which scanner engine is active.

    root@kitploit:~
    [pre_flight_scan]
    # Exclude the GitHub API spec (example tokens) and all test fixtures
    scan_exclude_paths = docs/api\.github\.com\.json
        tests/fixtures/
    

    exclude_emails — suppress email findings for specific addresses or entire domains. Values are newline/comma-separated, case-insensitive. Entries starting with @ match all emails at that domain; otherwise the entry must match the full address exactly. Applies to both working-tree and git history findings.

    root@kitploit:~
    [pre_flight_scan]
    # Suppress bot addresses and placeholder domains
    exclude_emails = [email protected], [email protected], @example.com
    

    Scanner configuration

    root@kitploit:~
    [pre_flight_scan]
    scan_for_secrets = true
    scan_for_emails = true
    scan_for_todos = true
    max_file_size_mb = 100
    
    # Scan git history for email addresses (requires scan_for_emails = true)
    # scan_email_history = true
    
    # Scanner selection: auto | native | docker | off
    #   auto   — try native truffleHog, fall back to container (podman/docker), then regex (default)
    #   native — native truffleHog only; no container fallback
    #   docker — container only; skip native PATH check
    #   off    — regex scanner only, no truffleHog attempt
    # trufflehog_mode = auto
    
    # Flag AI context files (CLAUDE.md, AGENTS.md, .cursorrules, etc.) as critical findings.
    # Their git history may contain more sensitive content than the current version.
    # warn_ai_context_files = true
    
    # Literal strings to flag as critical findings (case-insensitive).
    # Comma-separated or one per line (continuation lines must be indented).
    # banned_strings = secret
    #     password
    #     credential
    
    # Exclude files/directories from all scan checks (regex patterns, comma/newline separated).
    # The same patterns are passed to truffleHog via --exclude-paths.
    # scan_exclude_paths = docs/api\.github\.com\.json
    #     tests/fixtures/
    
    # Suppress email findings for specific addresses or entire domains (case-insensitive).
    # Entries starting with @ match all emails at that domain; otherwise exact address match.
    # exclude_emails = [email protected], [email protected], @example.com
    

    When banned strings or AI context files are found the scanner prints a ready-to-run git filter-repo command to remove them from the source repo's history before re-running.


    Configuration

    gh-safe-repo looks for configuration in this order (first match wins):

    1. --config PATH — explicit override
    2. ./gh-safe-repo.ini — current working directory
    3. $XDG_CONFIG_HOME/gh-safe-repo/gh-safe-repo.ini — defaults to ~/.config when $XDG_CONFIG_HOME is unset

    Bare --config (no path) skips file lookup entirely and uses built-in defaults only. All values have safe defaults — no config file is required to get started.

    A fully-annotated example config is included in the repository as gh-safe-repo.ini.example. Copy it to get started:

    root@kitploit:~
    # User-level config (XDG)
    mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/gh-safe-repo"
    cp gh-safe-repo.ini.example "${XDG_CONFIG_HOME:-$HOME/.config}/gh-safe-repo/gh-safe-repo.ini"
    
    # Or project-level config (current directory)
    cp gh-safe-repo.ini.example ./gh-safe-repo.ini
    

    Full configuration reference

    root@kitploit:~
    [repo]
    # Whether new repos are private by default
    private = true
    
    # Disable features that create clutter if unused
    has_wiki = false
    has_projects = false
    has_issues = true
    
    # Auto-delete head branches after merge (default: off, matching GitHub)
    delete_branch_on_merge = false
    
    # Merge strategies (all enabled by default, matching GitHub)
    # Set allow_merge_commit = false for squash-only workflows
    allow_squash_merge = true
    allow_merge_commit = true
    allow_rebase_merge = true
    
    # Whether a plain `create` leaves an initialized README in the new repo.
    # false (default): the repo still gets a default branch (needed for branch
    #   protection), but the auto-generated README.md is removed afterward.
    # true: keep the initialized README.
    # (Ignored for --local/--from, which always push your own history instead.)
    auto_init = false
    
    
    [actions]
    # Which actions are allowed to run: all | local_only | selected
    allowed_actions = selected
    
    # When allowed_actions = selected, control which external actions are permitted:
    github_owned_allowed = true       # actions maintained by GitHub (e.g. actions/checkout)
    verified_allowed = true           # actions from Marketplace verified creators
    # patterns_allowed = myorg/*      # comma-separated allowlist (wildcards OK)
    
    # Principle of least privilege: read-only by default
    # Options: read | write
    default_workflow_permissions = read
    
    # Prevent Actions from self-approving pull requests
    can_approve_pull_request_reviews = false
    
    # Require workflows to pin actions to a specific commit SHA instead of a mutable tag
    sha_pinning_required = true
    
    
    [branch_protection]
    # Applied to public repos on any plan, and private repos on paid plans.
    
    # Branch to protect
    protected_branch = main
    
    # Require a pull request before merging
    require_pull_request = true
    
    # Number of approvals required
    required_approving_reviews = 1
    
    # Dismiss existing approvals when new commits are pushed
    dismiss_stale_reviews = true
    
    # Require all review comments to be resolved before merging
    require_conversation_resolution = true
    
    # Do not enforce rules on administrators
    # false = repo owner can still push directly (needed for --from mirror workflow)
    enforce_admins = false
    
    # Block force-pushes
    allow_force_pushes = false
    
    # Block branch deletion
    allow_deletions = false
    
    # Use the Rulesets API (default) instead of the legacy classic branch-protection
    # path. A single ruleset covers all configured branches, supports bypass actors,
    # and is GitHub's forward direction (new rule types are Rulesets-only). Set false
    # to fall back to the classic per-branch API, which is kept for one release cycle.
    use_rulesets = true
    
    
    [tag_protection]
    # Immutable tags via Rulesets API.
    # Only works on public repos or paid GitHub plans (same restriction as branch protection).
    # Glob pattern(s) for tags to protect — comma-separated.
    protected_tags = *
    
    # Prevent deletion of matching tags (git tag -d / git push --delete)
    prevent_tag_deletion = true
    
    # Prevent rewriting matching tags (git tag -f / force-push)
    prevent_tag_update = true
    
    
    [security]
    # Enable Dependabot vulnerability alerts
    enable_dependabot_alerts = true
    
    # Auto-open PRs to fix vulnerable dependencies
    enable_dependabot_security_updates = true
    
    # Let security researchers report vulnerabilities privately
    enable_private_vulnerability_reporting = true
    
    # Block commits that contain supported secrets
    enable_secret_scanning_push_protection = true
    
    # Note: The following features have no REST API and must be configured via UI or dependabot.yml:
    #   - Grouped security updates: use dependabot.yml groups with applies-to: security-updates
    #   - Automatic dependency submission: enable via repository settings UI
    #   - Dependency graph: automatic for public repos; enable via UI for private repos
    
    
    [pre_flight_scan]
    scan_for_secrets = true
    scan_for_emails = true
    scan_for_todos = true
    
    # Flag files larger than this threshold
    max_file_size_mb = 100
    
    # Scan git history for email addresses (requires scan_for_emails = true)
    # scan_email_history = true
    
    # Scanner selection: auto | native | docker | off
    # auto   = try native truffleHog, fall back to container (podman/docker), then regex
    # native = native PATH only
    # docker = container only
    # off    = regex only
    # trufflehog_mode = auto
    
    # Flag AI context files (CLAUDE.md, AGENTS.md, .cursorrules, etc.) as critical findings.
    # warn_ai_context_files = true
    
    # Literal strings to flag as critical findings (case-insensitive).
    # Comma-separated, or one per line with continuation indentation.
    # banned_strings = secret
    #     password
    #     credential
    
    # Exclude files/directories from all scan checks (regex patterns, comma/newline separated).
    # Passed to truffleHog via --exclude-paths as well as applied to the regex walk.
    # scan_exclude_paths = docs/api\.github\.com\.json
    #     tests/fixtures/
    
    # Suppress email findings for specific addresses or entire domains (case-insensitive).
    # Entries starting with @ match all emails at that domain; otherwise exact address match.
    # exclude_emails = [email protected], [email protected], @example.com
    
    [git_transport]
    # How git push/clone authenticates when using --local or --from: auto | user_creds | token
    #   auto       — use your own git credentials (SSH key or credential helper) when a
    #                path exists; fall back to pushing over HTTPS with the API token in
    #                the URL only when there is no SSH setup and no credential helper
    #                (e.g. CI with just GITHUB_TOKEN). (default)
    #   user_creds — never use the API token for git. Pushes with your own credentials
    #                only; this avoids needing the `workflow` token scope to push
    #                .github/workflows files.
    #   token      — always push over HTTPS with the API token in the URL. For CI where
    #                the token was granted the `workflow` scope intentionally.
    # mode = auto
    

    GitHub Plan Limitations

    Some features are only available depending on repo visibility and your GitHub plan.

    FeatureFree + PublicFree + PrivatePro/Team + Private
    Branch protection / RulesetsYesNoYes
    Tag protection (Rulesets)YesNoYes
    Dependabot alertsYesNoYes
    Dependabot security updatesYesNoYes
    Secret scanningAutoNoYes
    Push protectionYesNoYes
    Private vulnerability reportingYesYesYes
    Dependency graphAutoNoYes

    gh-safe-repo detects your plan level and repo visibility at runtime. Unavailable features appear as SKIP in the plan output with a clear reason — the tool never fails silently.


    How It Works

    root@kitploit:~
    gh-safe-repo create <owner/repo>
          │
          ├─ Parse owner/repo, validate owner matches authenticated user (create only)
          ├─ Load config (./gh-safe-repo.ini or $XDG_CONFIG_HOME/gh-safe-repo/gh-safe-repo.ini)
          ├─ Apply CLI flag overrides (--public, etc.)
          ├─ Authenticate via gh CLI or GITHUB_TOKEN
          ├─ GET /user → owner login + plan level  (single cached call)
          │
          ├─ Build plan (each plugin compares desired vs. current state)
          │   ├─ RepositoryPlugin  → repo creation + basic settings
          │   ├─ ActionsPlugin     → allowed actions, workflow permissions, SHA pinning
          │   ├─ BranchProtectionPlugin → Rulesets API (default; classic if use_rulesets = false)
          │   ├─ SecurityPlugin    → Dependabot, secret scanning, push protection, private vuln reporting
          │   └─ TagProtectionPlugin → immutable tags via Rulesets API
          │
          ├─ Print plan table
          │
          └─ Apply (unless --dry-run)
              ├─ POST /user/repos
              ├─ PATCH /repos/{owner}/{repo}       (settings)
              ├─ PUT  /repos/{owner}/{repo}/actions/permissions/workflow
              ├─ POST/PATCH /repos/{owner}/{repo}/rulesets  (branch protection; default)
              │   or PUT /repos/{owner}/{repo}/branches/main/protection (if use_rulesets = false)
              ├─ PUT  /repos/{owner}/{repo}/vulnerability-alerts
              ├─ PUT  /repos/{owner}/{repo}/automated-security-fixes
              ├─ PUT  /repos/{owner}/{repo}/private-vulnerability-reporting
              ├─ PATCH /repos/{owner}/{repo}  (security_and_analysis: push protection)
              ├─ POST /repos/{owner}/{repo}/rulesets  (tag protection ruleset)
              ├─ git clone --mirror + git push --mirror (if --from)
              └─ git clone <local> + git push --all --tags (if --local, git repo)
                  or git init + add -A + commit + push (if --local, plain dir)
    

    Plugin architecture

    Each category of settings is a self-contained plugin class (gh_safe_repo/plugins/). Every plugin:

    1. Fetches current state from the GitHub API
    2. Compares against desired state from config
    3. Returns a Plan (list of Change objects: ADD / UPDATE / DELETE / SKIP)
    4. Applies only real changes — no API calls for no-ops

    This means audit mode and create mode use the same plan/apply path. The only difference is whether current state is fetched from an existing repo or assumed to be GitHub defaults.

    Authentication

    API calls resolve a token in this order:

    1. GITHUB_TOKEN environment variable — lets you target a specific account without switching the active gh session (and is the only credential needed in CI)
    2. gh auth token — whatever gh auth login set up
    3. Error if neither is available

    Tokens are passed to child gh api processes as GH_TOKEN in the subprocess environment and are never logged.

    Git operations (--local / --from push and clone) use your own git credentials — SSH key or credential helper — by default, not the API token. In environments with neither (e.g. CI with only GITHUB_TOKEN), the tool falls back to pushing over HTTPS with the token in the URL; the [git_transport] mode config setting controls this (see the configuration reference). Token-bearing URLs are never written to your repo's .git/config and are redacted from all output.

    API approach

    All GitHub API calls go through gh api via subprocess. This keeps authentication entirely in the gh CLI — no token management code, no OAuth flow, no PyGithub version pinning. JSON request bodies are passed via --input - (stdin), not --field flags.


    Development

    root@kitploit:~
    # Clone and set up
    git clone https://github.com/your-username/gh-safe-repo
    cd gh-safe-repo
    uv sync                          # creates .venv, installs pytest
    
    # Run tests
    uv run pytest tests/ -v
    
    # Run the tool directly (without installing)
    ./gh-safe-repo create <owner/repo> --dry-run
    
    # Install globally (picks up the current source)
    uv tool install .
    

    See tests/README.md for test file descriptions, mocking conventions, and how to add new tests.

    Project structure

    root@kitploit:~
    gh-safe-repo/
    ├── gh-safe-repo          # Thin launcher (entry point for direct use)
    ├── gh_safe_repo/         # Package — see gh_safe_repo/README.md for internals
    │   ├── cli.py            # Subparser dispatch (create, fix, scan)
    │   ├── commands/         # Subcommand implementations
    │   │   ├── _common.py    # Shared helpers, CLIContext, plan formatting
    │   │   ├── create.py     # create subcommand
    │   │   ├── fix.py        # fix subcommand
    │   │   └── scan.py       # scan subcommand
    │   └── plugins/          # Settings plugins (one per category)
    ├── pyproject.toml        # Build config, entry points
    ├── gh-safe-repo.ini.example  # Fully annotated example config
    └── tests/
    

    See gh_safe_repo/README.md for the module map, plugin architecture, and a guide to adding new settings.

    Dependency policy

    There are no runtime dependencies. Everything uses the Python standard library (argparse, configparser, subprocess, json, re). Do not add third-party packages without discussion.

    pytest is the only dev dependency, declared as a UV-native [dependency-groups] entry in pyproject.toml.


    Prior Art

    These projects were studied during design and influenced the architecture of gh-safe-repo. They are distinct tools with different scope and user models — see docs/LEARNINGS.md for detailed technical notes on how patterns were adapted.

    • github/safe-settings — Org-level GitHub App (Node.js/Probot) that enforces repository settings from a central config. Source of the plugin architecture pattern (one class per setting category, fetch → diff → apply) and the mergeDeep comparison approach.

    • repository-settings/app — Simpler per-repo variant of safe-settings, also Node.js/Probot. Provided a cleaner reference for the Diffable base plugin pattern.

    • nicholasgasior/gh-repo-settings — CLI extension written in Go with a plan/apply workflow. Primary inspiration for the gh api subprocess wrapper pattern and the dry-run plan output design.

    Download Tool