Protect the Default Branch from Direct Agent Commits

Stops the agent from committing straight to main or master. Changes have to land on another branch and go through a pull request, where a person reviews them.

On this page
  1. What it does
  2. Compliance alignment
  3. Why ingress
  4. Configuration
  5. Tool name matching
  6. Examples
  7. Allowed
  8. Denied
  9. Composition
  10. Known limitations
  11. Policy source (Rego)
  12. Related policies
Direction
ingress (tool_pre_invoke)
Default
deny on match, allow otherwise
Rego package
github.ingress.protect_default_branch
App
GitHub
Bundles
Engineering Stack JiraEngineering Stack Linear
Published
Minimum gateway
1.0.0b24
Schema version
1.0.0
Checksum
sha256:52290d582789f6b5c2c34ab73a17640a239dcc73cf889dc55a2af54bc4edaa56

What it does

Stops the agent from committing straight to main or master. Changes have to land on another branch and go through a pull request, where a person reviews them.

  • create_or_update_file, push_files, and delete_file are denied when their branch argument is main or master (case-insensitive, with an optional refs/heads/ prefix).
  • The same calls are denied when branch is missing or empty. GitHub's contents API writes to the repository's default branch when no branch is given, so an omitted branch is treated as a commit to the default branch.
  • Commits to any other branch, and every other tool (reads, branch creation, PRs), pass through.

Compliance alignment

  • SOC 2 CC8.1 — supports change management: changes reach the production branch only through a reviewed pull request.
  • SOX ITGC (change management) — supports segregation between authoring a change and promoting it.

Why ingress

A commit is permanent once it reaches GitHub, and a push to the default branch can trigger deploys immediately. The target branch is in the request, so ingress can stop it before anything is written.

Configuration

protected_branches lists the branch names treated as protected. Add develop, release, or trunk if your team uses them as the default or deploy branch.

Tool name matching

Tools are matched by suffix (create_or_update_file, push_files, delete_file), so the policy works whatever name your gateway gives the GitHub server, and covers both the official github/github-mcp-server and the archived community server, which use the same names for these three tools. Confirm the exact names your gateway sends with the dump-input debug technique before relying on it.

Examples

Allowed

{
  "input": {
    "action": "tool_pre_invoke",
    "resource": { "name": "github-push_files", "type": "tool" },
    "payload": { "args": { "owner": "acme", "repo": "app", "branch": "fix/payments-timeout",
      "message": "Revert timeout", "files": [ { "path": "src/client.ts", "content": "..." } ] } }
  }
}

allow = true, no reason.

Denied

{
  "input": {
    "action": "tool_pre_invoke",
    "resource": { "name": "github-create_or_update_file", "type": "tool" },
    "payload": { "args": { "owner": "acme", "repo": "app", "branch": "main",
      "path": "src/client.ts", "message": "hotfix", "content": "..." } }
  }
}

allow = false, reason = "Blocked: agents can't commit directly to the main or master branch. Push to a new branch and open a pull request instead.".

Composition

Pairs naturally with require-human-approval-merge: this policy forces changes into a PR, that one keeps the merge with a person. Together they mean nothing an agent writes reaches the default branch without review.

Known limitations

  • The default branch name is not looked up. The policy can't see each repository's configured default branch. A repo whose default is trunk or develop is unprotected until you add that name to protected_branches.
  • Other write paths are out of scope. merge_pull_request (handled by require-human-approval-merge), update_pull_request_branch, and Git operations made outside the MCP server are not covered.
  • Not a substitute for GitHub branch protection. Keep branch protection rules on in GitHub too; this policy covers the agent path only.

Compliance note. This policy supports alignment with the cited framework controls on the MCP path only. No policy or bundle makes an organization compliant with any framework; web-UI, native-API, and in-app access are outside the gateway's reach by design. Validate against your own compliance program before relying on it.

Policy source (Rego)

package github.ingress.protect_default_branch

# Allow by default; deny commit-writing tools that target a protected branch.
default allow := true

# Branch names treated as the default / deploy branch. Extend for your repos.
protected_branches := {"main", "master"}

_tool := lower(object.get(input.resource, "name", ""))

_args := object.get(object.get(input, "payload", {}), "args", {})

# Tools that write a commit to a named branch.
_commit_tool_suffixes := {"create_or_update_file", "push_files", "delete_file"}

_is_commit_tool if {
    some suffix in _commit_tool_suffixes
    endswith(_tool, suffix)
}

# Normalize the requested branch: trim, lowercase, drop a refs/heads/ prefix.
_branch := b if {
    raw := object.get(_args, "branch", "")
    is_string(raw)
    b := trim_prefix(lower(trim_space(raw)), "refs/heads/")
}

# Missing, non-string, or empty branch: GitHub writes to the default branch.
_targets_default if not _branch

_targets_default if _branch == ""

_targets_protected if _targets_default

_targets_protected if protected_branches[_branch]

allow := false if {
    _is_commit_tool
    _targets_protected
}

reason := "Blocked: agents can't commit directly to the main or master branch. Push to a new branch and open a pull request instead." if {
    _is_commit_tool
    _targets_protected
}

Canonical source: policy.md on GitHub · raw · raw on this site (.md)

↑↓ to navigate ↵ to open

Get in touch

Tell us what you need to govern.

A few lines is plenty — what you're deploying, and what you need control over. We read every one.