FeatureAgents/AgentsGitFlowController

FeatureAgents/AgentsGitFlowController

AI 编码智能体分支角色守卫:可配置集成/预览/生产/归档角色,拦截对受保护分支的直接推送、强推与删除,以及智能体对生产/归档分支的合并。

Git 与代码评审ai-agentsclaude-codecodex

agents-gitflow-guard

Are you tired of agents skipping your GitFlow?

A configurable branch-role guard for AI coding agents — Claude Code, Codex, OpenCode, Antigravity, CodeBuddy, ZCode, Cursor, DeepSeek Harness (DSH), and Pi. You define your own branches — integration (features merge in via PR/MR), preview (env endpoints), production, archive — each with its own update rules. Agents can't skip the flow, and sensitive merges stay in your hands.

English · 简体中文 · 繁體中文 · 日本語 · 한국어 · Deutsch · Français · Italiano · Português · Español · Русский · License

Table of Contents

  • Quick Start — 30 seconds to a guarded repo
  • Why — the problem this plugin solves
  • Who this is for — scenarios & teams
  • What it does — capabilities
  • What it does NOT do — honest limits
  • Server-side protection vs this plugin
  • How it works — the mechanism in three lines
  • Configuration Reference
  • Gate Matrix — what gets blocked, what passes
  • Where the human stays in control
  • Installation in detail
  • FAQ
  • Glossary
  • Roadmap
  • Development
  • Support
  • License

Quick Start — 30 seconds to a guarded repo

Step 1 — install. All nine clients consume the same npm package agents-gitflow-guard — choose the installation mode matching your agent:

# Mode A: CLI Hook clients (Claude Code · Codex · OpenCode · Antigravity · CodeBuddy · ZCode · Cursor)
npm i -g agents-gitflow-guard
# Mode B: DSH in-process plugin (restart DSH afterwards; plugins load at startup)
dsh plugin --profile web add agents-gitflow-guard
# Mode C: Pi in-process extension
npm i -D agents-gitflow-guard
Note: A bare add or npm i installs the latest version from npm registry. If your registry mirror has a cache delay or you need to lock to a specific version, append @ (e.g. npm i -g agents-gitflow-guard@). The DSH-specific peer dependencies (@deepseek-ai/cordis / @deepseek-ai/dsh-tools) are declared optional — only the DSH in-process plugin needs them, and DSH supplies them through its shared profile module fallback at runtime; CLI / Pi / OpenCode users are not forced to install them.
CLI hook clients perform one wiring command after install (see Step 2); Pi copies an extension file; DSH auto-mounts on plugin installation.

Step 2 — wire your client (no config file needed). The guard ships with built-in defaults that protect `develop` (integration) + `main` (archive) — zero configuration, on by default. The only thing you need is to tell your AI client to invoke the guard, with one command per stdin-hook client (DSH is wired automatically; Pi just copies a file, see below):

# Claude Code → this repo's .claude/settings.json
gitflow-guard wire --client claude --project --yes
# Codex / OpenCode / Antigravity / CodeBuddy / ZCode / Cursor (each its own config file; --yes skips the y/N prompt)
gitflow-guard wire --client codex --project --yes
gitflow-guard wire --client opencode --project --yes
gitflow-guard wire --client antigravity --project --yes
gitflow-guard wire --client codebuddy --project --yes
gitflow-guard wire --client zcode --project --yes
gitflow-guard wire --client cursor --project --yes
# Preview (no writes) / remove / interactive guide:
gitflow-guard wire --client claude --dry-run
gitflow-guard wire --client claude --unwire
gitflow-guard setup

wire merges into your existing config non-destructively (already-present hooks are left untouched; legacy gitflow-guard entries are migrated to the current self-anchored form on re-run), and writes to your project dir by default — --global (all repos on this machine) always asks first or needs --yes. Per-client files and formats are mirrored in Installation in detail.

⚠️ main is protected by default. Trunk / single-branch users (everyone pushes straight to one branch) will get blocked on direct main pushes until they opt out — create gitflow-guard.config.json with { "enabled": false }, or map your own branches (see Configuration Reference). gitflow-guard status repeats this notice whenever the built-in defaults are in effect.

Step 3 — verify. Ask the agent to git push origin develop. Expect the tool call to be denied:

Error: [gitflow-guard] blocked: Protected branch "develop" forbids direct push
Next: Integration branch (develop) is updated via PR/MR from a feature branch: push the feature first, then `gh pr create --base develop` / `glab mr create --target-branch develop`.

Messages are English by default; create a config with "locale": "zh" to switch to Chinese — messages then read like: *已拦截: 受保护分支「develop」禁止直推 / 下一步: 集成分支(develop)由 PR/MR 合入 feature……* (see Configuration Reference).

Done. The guard is live for this repo with the built-in defaults. Want more stages (preview / production) or different branch names? Write a gitflow-guard.config.json and only the fields you care about — everything else keeps the built-in defaults. For the full decision table, see the Gate Matrix.

Full walkthrough — one feature, end to end

Scenario: your team ships a login page (feature/login-page); develop is the integration branch, main the archive. What you and the agent experience at every step:

# · what the agent runs · plugin decision · what you see 1 · git checkout -b feature/login-page (from develop) · ✅ allow (feature work is free) · branch created 2 · git add . && git commit -m "feat: login" · ✅ allow · committed 3 · git push -u origin feature/login-page · ✅ allow (pushing your feature is fine) · pushed 4 · git checkout develop && git merge feature/login-page · 🚫 deny — integration branch is PR/MR-only · must open a PR/MR into develop 5 · gh pr create --base develop · ✅ allow (feature → integration via PR) · PR created, you review & merge 6 · git push origin main or merge into main · 🚫 deny — archive is user-hand only · you archive develop → main yourself after release

Notice what the agent *cannot* do: merge a feature straight into develop, or touch main at all. Every sensitive merge is a deliberate human action in the PR/MR page or your own terminal.

Why — the problem this plugin solves

AI coding agents work in your repository. They are *told* — via system prompts, project instruction files (AGENTS.md, CLAUDE.md, GEMINI.md, .cursorrules, and similar), and project docs — to follow a merge flow: develop on a feature branch, merge into the integration branch (and your preview/production stages when you have them), and leave archive/production merges to you.

That is a soft rule. Agents skip it, reorder it, or simply "forget" it — not out of malice, but because soft instructions are optional to a model.

This plugin turns the soft rule into a hard mechanism. Every git operation an agent attempts is checked against the *actual state of your local repository*. Violations are blocked before the command runs, with an explanation of why and what to do next.

Nobody has to remember the rules — the rules are enforced.

Who this is for — scenarios & teams

Signs this plugin is for you

  • You have — or want — a defined branch flow, from a single develop-style integration branch up to multi-stage preview/production pipelines.
  • An agent has already cut a corner: pushed straight into a protected branch, or merged somewhere it shouldn't. If it happened once, it will happen again — this plugin is the structural fix.
  • You protect your integration/archive branches but don't want to rely on human review to catch every shortcut.
  • Multiple features develop in parallel and land in one shared preview environment, and you want each entry to a stricter stage reviewed.

Concrete scenarios

  1. Solo developer + agent on client projects. You hand the agent a ticket; it "helps" by pushing straight into the integration branch. One small config file, and the agent physically cannot touch protected branches without a PR/MR — even when you're not watching.
  2. Small team (3–10) with a CI-deployed preview. Staging auto-deploys on merge; one day an agent merged a feature into develop without review. From then on, every entry to the protected stages requires a PR/MR — a deliberate, audited act.
  3. Enterprise with multi-env pipelines. Many preview endpoints plus a gated production and archive line — each role simply gets configured, and the guard scales without extra rules.
  4. Async collaboration. You're not always online. The guard keeps the flow honest between your sessions; production/archive merges remain yours alone.

Not for you (see also What it does NOT do):

  • Trunk-based flow — everyone merges straight to one branch: the plugin would block constantly.
  • Personal repo without a defined flow — nothing to enforce, no value.
  • A team unwilling to give any branch a role — the plugin needs at least one integration branch to protect.

What it does — capabilities

  • Blocks, before execution: direct push / force-push / delete of protected role branches (integration / preview / production / archive); agent merging into production or archive.
  • Role-driven, fully configurable: integration (built-in default: develop) is the core role; preview / production / archive are optional arrays of branch names or regexes, each with its own update rules (pr / flexible, mergeBy).
  • Merge-by-user where it matters: production & archive merges stay in your hands — the plugin blocks the agent from clicking merge, so your action *is* the confirmation.
  • Works with any naming: branch names are mapped by your config, never hard-coded (see Configuration).
  • Fully audited: every deny is appended to an audit log under your user state directory (~/.local/state/gitflow-guard/, %LOCALAPPDATA%\gitflow-guard on Windows) — outside the repository, never committed, outside the agent's writable sandbox, and shared across all linked worktrees of one repository.
  • Platform-agnostic core: pure local git; optionally consults gh (GitHub) or glab (GitLab) for PR/MR target resolution, and works fine without them.

What it does NOT do — honest limits

  • It is not a security boundary. Command parsing is best-effort; an agent determined to obfuscate commands can evade text analysis.
  • It does not gate on CI platforms. CI status is logged as a reference only, never a hard gate. Real branch protection belongs in GitHub/GitLab settings, which can layer on top.
  • It is not a replacement for the flow itself. Your project must have at least one integration branch; if everyone pushes straight to one branch, this plugin will block constantly — don't enable it there.
  • Production/archive are not automated — they're deliberately left to your human click; the plugin only tells agents "no".

Server-side protection vs this plugin

Server-side branch protection (GitHub branch rules, GitLab protected branches) and this plugin solve different problems. They are complementary, not alternatives.

dimension · server-side protection · this plugin what it governs · *who* may push / merge to protected branches (permissions) · *how* agents may enter the flow (workflow) — which role a merge lands in keeps agents from merging into production/archive · no — it can't tell "agent did it" · yes — production/archive merges are blocked for agents by default per-role flexibility · one rule per branch on the host · per-role update (pr/flexible) + mergeBy (user/anyone) in one config file scope · every user of the repository, humans included · agents with the hook or plugin configured (humans are not restricted) enforcement point · server-side, at push / merge time · local, before the command runs platform · tied to the hosting service · pure local git, platform-agnostic (gh / glab optional) bypassable by · users with admin rights · any agent not wired to the guard, or a determined malicious agent

Why this matters: branch protection answers *"can this push happen at all?"*; this plugin answers *"may this agent enter this role, given the config?"*. The strongest setup uses both — the plugin keeps agents honest about the workflow, and branch protection guarantees that no one, agent or human, pushes straight to a protected branch.

How it works — the mechanism in three lines

  1. An agent calls a shell tool (pwsh / bash) with a git command.
  2. The plugin classifies the command, resolves the branch roles from gitflow-guard.config.json, and applies the gate matrix.
  3. Violation → the tool call is denied before it runs, with a reason and the next step. Allowed → the command proceeds; every deny is audited to the user-level log (~/.local/state/gitflow-guard/repos/-/audit.jsonl).

No chat-confirmation or permit store: sensitive merges (production / archive) are simply user-only — an agent may prepare the PR/MR, but the merge click stays yours.

Design principles — why it works

1. Config is the single source of truth

Nothing about branch names or rules is hard-coded. integration ships as a built-in default (develop); preview / production / archive are optional arrays of exact names or regexes, each with its own update and mergeBy — deep-merged over the defaults. The same binary scales from a solo develop to an enterprise multi-env pipeline.

2. Blocking happens before execution, not after

The guard hooks each platform's pre-tool event — DSH tools/pre-execute, Pi tool_call, and the CLI clients' PreToolUse hook — the decision point that runs *before* the command is dispatched. A deny there means the command never runs; the agent only ever sees the rejection. Post-hoc detection (scanning logs after the fact) can't work as enforcement — the damage would already be done.

3. The sensitive merges are unforgeably human

No plugin code decides "is this merge OK?" for production or archive. The gate simply refuses to let an *agent* perform those merges, so the only path is a PR/MR page where you click merge — and that click is the confirmation. There is no token, permit, or chat message an agent could forge to get past you.

Configuration Reference

Built-in defaults & deep-merge override

The guard is on by default — no gitflow-guard.config.json needed. It protects:

default · role · rule develop · integration · no direct push; updates via PR/MR (update: "pr") main · archive · no direct push / no agent merge; the archive merge is yours (mergeBy: "user")

When you do create gitflow-guard.config.json, its fields are deep-merged over the defaults: each field/role you write replaces the default for that field/role, everything you don't write keeps the default. Write only what you want to change:

{
  "branches": { "production": ["release-[\\w-]+"] }  // defaults keep develop+main; production is added
}

Disable entirely (trunk / single-branch flows): { "enabled": false }. Fixing an accidental block is a one-file change, and gitflow-guard status always explains what is in effect (including when it is the built-in defaults).

Branch roles — the model behind the checks

A role maps branch names (or regexes) to a rule set. integration is provided by the defaults; every other role is optional.

feature branches ──(free)──> integration (integration branch; updates via PR/MR)
                                   │
                                   ├──> preview (optional; env endpoints; updates via PR/MR)
                                   │
                                   └──> production (optional; PR/MR + only you click merge)
archive (optional; you archive after release)

role · config key · required? · enforced behavior feature · featurePattern · — · free: commit / push / sync / rebase integration · branches.integration · default (develop) · no direct push (default pr); features merge in via PR/MR preview · branches.preview (array) · optional · no direct push; updates via PR/MR only (env endpoints) production · branches.production (array) · optional · PR/MR only; merge by user only (mergeBy: "user") archive · branches.archive (array) · default (main) · archive PR/MR may be created by agents; the merge stays user-hand only

Customizing branch names & rules — any naming works

Small team (solo / 2–3 devs) — minimal: integration only:

{
  "enabled": true,
  "featurePattern": "feature/[\\w-]+",
  "branches": { "integration": ["develop"] }
}

Larger team (multiple preview envs + production + archive):

{
  "enabled": true,
  "featurePattern": "(topic|feature)/[\\w-]+",
  "branches": {
    "integration": ["develop", "topic/[\\w-]+"],
    "preview": {
      "branches": ["ita1", "itb1", "itb2", "sg", "vb", "r1-conf", "r1-ope", "r2-conf", "r2-ope"],
      "update": "pr"
    },
    "production": {
      "branches": ["prd-conf", "prd-ope"],
      "update": "pr",
      "mergeBy": "user"
    },
    "archive": ["main"]
  }
}

Full field reference

{
  "enabled": true,                     // default true — set false to turn the guard off
  "featurePattern": "feature/[\\w-]+", // JS regex matching your working/feature branches
  "branches": {
    "integration": { "branches": ["develop"], "update": "pr" },  // default: ["develop"] — omit to keep
    "preview":     { "branches": ["ita1"], "update": "pr" },     // optional
    "production":  { "branches": ["prd"], "update": "pr", "mergeBy": "user" }, // optional
    "archive":     ["main"]                                      // optional
  },
  "worktree": {                        // optional: working tree and upstream baseline guard
    "requireCleanOnPr": false,         // require clean staged/unstaged changes before creating PR (default false)
    "requireCleanOnMerge": false,      // require clean working tree before merging (default false)
    "allowUntracked": true,            // allow untracked files (??); false blocks if untracked exist (default true)
    "requireUpstreamSynced": false     // require branch to be synced with upstream baseline (default false)
  },
  "locale": "en",                      // optional: message language — any registered locale ('en'/'zh' built-in); unknown values warn in status and fall back to English
  "strict": false,                     // optional: fail-closed — invalid config / internal errors block instead of warn-and-allow
  "ci": { "enabled": true }            // optional: gh pr checks logged as reference
}
  • Roles accept either an array (shorthand) or an object { branches, update?, mergeBy? }.
  • update: pr (default) = updates only via PR/MR; flexible = allow direct/local merges (small teams).
  • mergeBy (production): user (default) = only you click merge; anyone = allow PR merge through.
  • Working tree & upstream baseline guard (`worktree`): optional state and divergence checks — requireCleanOnPr: true blocks PR creation if there are uncommitted staged/unstaged changes; requireCleanOnMerge: true blocks local and PR merges on dirty working trees; allowUntracked (default true) allows untracked files (??) without friction, or set false for strict human-agent collaboration; requireUpstreamSynced: true blocks PR creation when the branch is behind the upstream baseline (behind > 0). Multi-segment compound commands (e.g. git add . && git commit && gh pr create) dynamically simulate a clean state for subsequent segments.
  • Each branch entry is an exact name or a regex (auto-detected). Regex safety: branch patterns are authored by you and compiled as-is; a pattern is matched against the whole branch name (compiled as ^(?:)$), so write expressions that cover the entire name — substring-style patterns like feature/ or a bare fix won't match, and those branches fall through to other. Avoid catastrophic-backtracking constructs (e.g. nested quantifiers like (\w+)+) in featurePattern and branch entries.
  • Language: messages are English by default; add "locale": "zh" for Chinese, or pass --locale to any gitflow-guard subcommand (priority: CLI flag > project config > English). All user-facing text follows the locale — including CLI framework messages such as --help, unknown-command notices, and the empty-audit line.
  • Custom locales: downstream packages can add a language at runtime — import { registerLocale } from 'agents-gitflow-guard', call registerLocale('fr', frDict) with a dictionary covering exactly the same keys as built-in English (validated on registration), then set "locale": "fr" in the project config to activate it.
import { registerLocale, MESSAGE_KEYS } from 'agents-gitflow-guard'
  // MESSAGE_KEYS lists every key a dictionary must define (same set as built-in English);
  // registration throws if a key is missing or extra.
  const fr = { /* one entry per MESSAGE_KEYS, e.g. */ 'deny.header': ({ why }) => `[gitflow-guard] bloqué : ${why}` }
  registerLocale('fr', fr)
  • Unknown locales: an unregistered "locale" value falls back to English during interception (by design — hooks never stall on wording), so a typo is easy to miss; the one-line warning shows up in gitflow-guard status.
  • Validation: overlapping role entries are rejected; invalid regex is rejected. Any config error reverses the project to "not enabled" (reported) rather than applying a half-guessed setup; watch out that a role you override with the same branch name as a default role (e.g. mapping main to integration while the default archive is still main) is an overlap error — cover or drop the other role too.
  • Strict mode: by default a broken config warns on stderr once and lets the command pass (fail-open, so a typo can't wedge your tooling). "strict": true flips config errors and internal errors to block (fail-closed) — for high-risk repos. An explicit enabled: false stays silent; a *missing* file is not an error anymore