Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 92 additions & 134 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,113 +1,49 @@
# MergeWarden for AI PRs
# MergeWarden

[![Release](https://img.shields.io/github/v/release/sjh9714/mergewarden?label=release)](https://github.com/sjh9714/mergewarden/releases)
[![CI](https://github.com/sjh9714/mergewarden/actions/workflows/ci.yml/badge.svg)](https://github.com/sjh9714/mergewarden/actions/workflows/ci.yml)
[![MergeWarden](https://github.com/sjh9714/mergewarden/actions/workflows/mergewarden.yml/badge.svg)](https://github.com/sjh9714/mergewarden/actions/workflows/mergewarden.yml)
[![License](https://img.shields.io/github/license/sjh9714/mergewarden)](LICENSE)

**AI coding tools open pull requests faster than anyone can read them. MergeWarden
tells you which ones need a closer look, and why.**
**A pull request quietly edited your `CLAUDE.md`.**

## The situation it is built for
In the diff it reads as a documentation tidy-up. That file is what every coding
agent reads before it touches your repository, so the change outlives the pull
request and shows up in nothing anyone reviews afterwards.

An agent opens a pull request titled "Document the release process". The diff
touches four files. Three of them are documentation. The fourth is your release
workflow.
MergeWarden leaves one comment when that happens. **It closes nothing.**

Nothing in the title said that would happen, and in a busy week nobody notices.
MergeWarden notices, and leaves one comment saying so.
## What that actually looks like

It reports facts you could have checked by hand. It does not review your code,
grade the contributor, or merge anything.
This is a real pull request, titled "docs: tidy up the contributor notes", whose
whole diff is one added line in `CLAUDE.md`.

## See it work, without installing anything

This runs a small example that ships inside the tool. No token, no repository,
no network:

```bash
npx --yes mergewarden@0.10.1 demo
```

```
Repository demo-org/demo-service
Pull request #482 "Document the release process"
Author an agent on branch codex/document-releasing
Declared allowed_paths: docs/**
Changed docs/releasing.md, .github/workflows/release.yml, AGENTS.md, package.json

MergeWarden: NEEDS REVIEW

ERROR contract/out-of-scope
Message: .github/workflows/release.yml changed outside the allowed contract scope.
Path: .github/workflows/release.yml
```

Now point it at a real pull request. Public repositories need no token:

```bash
npx --yes mergewarden@0.10.1 scan owner/repository#123
```

![The full mergewarden demo report scrolling past in a terminal](docs/assets/mergewarden-demo.gif)

If a queue has built up, read the whole thing at once instead. This is real
output from a public repository:

```bash
npx --yes mergewarden@0.10.1 triage owner/repository
```

```
20 open pull request(s) read. 9 have something a maintainer checks by hand.

#6941 update-unmanaged-certificates no description · template unused
#7227 add-tests no linked issue · oversized
#7790 feat/dedup-dynamic-upstreams no linked issue · template unused
#7290 natsort template unused
#7669 webtransport-reverse-proxy oversized
#7878 rfc9440-client-cert-placeholders oversized
#7912 feat/fastcgi-server-addr template unused
#7913 slowloris-idle-timeout no linked issue
#7922 fix/network-proxy-missing-host no linked issue

Nothing was closed, labelled, or commented on.
Why: This file can change how AI agents behave in future PRs. (CLAUDE.md)
Next: Review the agent instruction/tooling change before merging.
```

Every row is something you would have checked by hand, and you can confirm any
of them in a few seconds. The other eleven pull requests are not listed because
there was nothing to say about them. Nothing is written back to GitHub, and the
command needs no write access.

**`triage` needs `GH_TOKEN` set, even for a public repository.** It reads every
open pull request, and GitHub allows 60 unauthenticated requests an hour, which
one queue uses up. Any personal access token with no scopes at all is enough,
since nothing here needs write access. Without one the command tells you so and
exits non-zero rather than reporting a half-read queue.

`scan`, on a single public pull request, works without a token.

There is deliberately no way to pass a token as a command-line flag, because
flags end up in shell history and CI logs.
[The pull request](https://github.com/sjh9714/agent-gate-install-smoke-20260617/pull/22)
and [the run that produced it](https://github.com/sjh9714/agent-gate-install-smoke-20260617/actions)
are public, so you can check the output against the diff yourself.

## What it looks for
It works whether or not an agent opened the pull request. That one says
`Agent detected: no`.

| It notices when | Which matters because |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------- |
| A pull request edits files outside what it said it would | An agent asked to update docs also changed your billing code |
| `AGENTS.md`, `CLAUDE.md`, `.mcp.json` or `.cursor/` change | These files instruct every future agent working in your repository |
| A workflow gains permissions it did not have before | It can now write where it could previously only read |
| Pull request text reaches an agent prompt | A stranger can put instructions in a title and have your automation follow them |
| An action, workflow or container is not pinned | Somebody else can change what runs in your CI without opening a pull request |
| An install script appears in a package manifest | That code runs during `npm install`, before any human reads it |
| Risky code changes with no matching test changes | Worth a look, not a verdict |
Watched by default: `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `QWEN.md`,
`.mcp.json`, `.cursor/`, `.codex/`, `.gemini/`,
`.github/copilot-instructions.md`, `claude_desktop_config.json`.

Every finding names the rule, the file, and the evidence, and carries an ID you
can quote when you discuss or dismiss it.
**How often is this?** We scanned 2,204 merged agent-authored pull requests on
public GitHub and **3.9%** of them changed a file like this. That is not a daily
event. It is the kind of thing you want to hear about the once it happens rather
than find six weeks later.

## Run it on every pull request
## Install

Create `.github/workflows/mergewarden.yml`:
One file, `.github/workflows/mergewarden.yml`:

```yaml
name: MergeWarden
Expand All @@ -126,21 +62,18 @@ jobs:
steps:
- uses: sjh9714/mergewarden@v0.10.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
mode: warn
fail-on-block: false
comment: auto
```

That is the whole setup. No checkout step, no build, no configuration file, and
nothing installed into your repository. Until you add a `mergewarden.yml` of
your own, a sensible default policy applies.
That is the whole setup. There is no checkout step, no build, no config file,
and no token to create: the Action uses the one GitHub already gives the job.
Nothing is blocked from merging until you decide otherwise.

Most pull requests cross no boundary. Those pass silently, and the details go to
the Actions job summary rather than to a comment. You hear from MergeWarden when
something is worth hearing about. Push a fix and the existing comment rewrites
itself to `PASSED` instead of being deleted, so a stale review request cannot
outlive the problem it described.
`comment: auto` is the only option that does anything on a fresh install. It
means MergeWarden stays silent when there is nothing to say, and everything else
goes to the Actions job summary. When it does comment, pushing a fix rewrites
that same comment to `PASSED` instead of deleting it, so a stale review request
cannot outlive the problem it described.

Pull requests from forks get a read-only token from GitHub, so they are never
commented on.
Expand All @@ -152,17 +85,40 @@ tag. MergeWarden does not publish or recommend a mutable `v0` tag.
- uses: sjh9714/mergewarden@c32fb900b65708ea7c875b8ec4244c0983343970
```

## What else it checks

Once it is installed, these come with it. Each one is a fact about the diff, not
a judgement about the contributor.

| It also notices | Because |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------- |
| A workflow gained a permission it did not have on the base | It can now write where it could previously only read |
| `pull_request_target` combined with a head checkout | That hands your repository's secrets to whoever opened the pull request |
| An action, reusable workflow or container on a moving tag | Somebody else can change what runs in your CI without opening a pull request |
| An install or prepare script added to a package manifest | That code runs during `npm install`, before any human reads it |
| Pull request text reaching an agent prompt | A stranger can put instructions in a title and have your automation follow them |
| Files outside the scope a pull request declared for itself | An agent asked to update docs also changed your billing code |

Every finding names the rule, the file, the evidence, and an ID you can quote
when you discuss or dismiss it. The
[configuration reference](docs/configuration.md) covers turning individual
checks up, down, or off.

## What it will not do

It does not judge your code. It has no opinion on style, naming, or whether a
change deserves to be merged, and it does not infer anything from how the
description is written.
**It never closes, labels, or merges anything.** There is no `--close` flag, and
a test in this repository asserts there never will be one. A pull request closed
by a bot in error does not get reopened by the person who gave up on it.

If what you want is for the pull requests to stop arriving, this is the wrong
tool and you should look at an auto-closing action instead. MergeWarden is for
deciding yourself, faster.

It never closes, labels, or merges anything. There is no `--close` flag, and a
test in this repository asserts there never will be one. A pull request that a
bot closes in error does not get reopened by the person who gave up on it.
It also does not judge your code. It has no opinion on style, naming, or whether
a change deserves to be merged, and it infers nothing from how the description is
written.

It is also careful about what it trusts:
On what it trusts:

- It does not execute pull-request code, and never checks the branch out.
- It reads your settings from the exact base commit of your branch, never from
Expand All @@ -173,44 +129,46 @@ It is also careful about what it trusts:
- If it cannot see everything it needs, it says so and fails rather than
reporting a pass it cannot stand behind.

The [security model](docs/security-model.md) explains the boundaries and the
known limits.
The [security model](docs/security-model.md) has the boundaries and the known
limits.

## Turning it on gradually
## Reading a whole queue at once

Nothing is blocked on day one. With `mode: warn` and `fail-on-block: false`,
MergeWarden reports and does nothing else. A reasonable order:
If pull requests have piled up, `triage` reads all the open ones and lists only
those with something you would have checked by hand.

1. Run it in `warn` for a week and read what it tells you.
2. Raise individual checks to `error` once you agree with them.
3. Set `mode: block` and `fail-on-block: true`.
4. Require the MergeWarden check in branch protection.
**This one needs a token**, even on a public repository, because it makes one
request per pull request and GitHub allows 60 an hour without one. A
[personal access token](https://github.com/settings/personal-access-tokens/new)
with **no scopes selected** is enough, since nothing here writes.

Each of those is one line in [configuration](docs/configuration.md).
```bash
export GH_TOKEN=github_pat_...
npx --yes mergewarden@0.10.1 triage owner/repository
```

Reports end with one of five verdicts, kept deliberately distinct:
```
20 open pull request(s) read. 9 have something a maintainer checks by hand.

- `PASSED`: analysis finished, nothing active to report.
- `OBSERVED FINDINGS`: evidence recorded, decision unchanged.
- `NEEDS REVIEW`: a person should look.
- `BLOCKED`: policy rejected the change.
- `ANALYSIS INCOMPLETE`: something could not be read, so no verdict is claimed.
#6941 update-unmanaged-certificates no description · template unused
#7227 add-tests no linked issue · oversized
#7790 feat/dedup-dynamic-upstreams no linked issue · template unused

Nothing was closed, labelled, or commented on.
```

Worried about noise? On 46 merged human pull requests it stayed quiet on 44, and
both of the findings it did raise were correct
([how that was measured](docs/study/what-a-zero-config-install-reports.md)). If
it ever flags something your team already decided was fine, please open an issue
with the output. That is the most useful bug report this project can get.
Without a token it tells you what it could not read and exits non-zero, rather
than showing you a queue it only half saw. [More on triage](docs/triage.md).

## Documentation

[Getting started](docs/getting-started.md) walks through installing it and
reading your first report. The [documentation index](docs/README.md) has
everything else, including the [configuration reference](docs/configuration.md).
[Getting started](docs/getting-started.md) walks through the first week. The
[documentation index](docs/README.md) has the rest, including the
[configuration reference](docs/configuration.md).

If you run the agents yourself rather than reviewing their pull requests, the
same engine ships as an [MCP server](packages/mcp/README.md). It checks a change
against the scope you gave the agent before a pull request exists.
Running the agents yourself rather than reviewing their pull requests? The same
engine ships as an [MCP server](packages/mcp/README.md) that checks a change
against the scope you gave it, before a pull request exists.

## Contributing

Expand Down
42 changes: 29 additions & 13 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,44 @@
# MergeWarden:面向 AI PR 的变更管控门禁
# MergeWarden

[![Release](https://img.shields.io/github/v/release/sjh9714/mergewarden?label=release)](https://github.com/sjh9714/mergewarden/releases)
[![CI](https://github.com/sjh9714/mergewarden/actions/workflows/ci.yml/badge.svg)](https://github.com/sjh9714/mergewarden/actions/workflows/ci.yml)
[![MergeWarden](https://github.com/sjh9714/mergewarden/actions/workflows/mergewarden.yml/badge.svg)](https://github.com/sjh9714/mergewarden/actions/workflows/mergewarden.yml)
[![License](https://img.shields.io/github/license/sjh9714/mergewarden)](LICENSE)

> **守在 AI 智能体与主分支之间的那道门。**

[English](README.md) · 简体中文

编码智能体整天都在提 PR。MergeWarden 是一道变更管控门禁,用只有你的仓库才能定义的边界去检查每一个 PR:
**某个 PR 悄悄改了你的 `CLAUDE.md`。**

在 diff 里它看着就是一次文档整理。可那个文件是每个编码智能体动你仓库之前都会读的东西,
所以这次改动会比这个 PR 活得更久,而且之后没有任何人会再去审它。

发生这种事时,MergeWarden 会留下一条评论。**它不会关闭任何 PR。**

## 实际长这样

下面是一个真实的 PR,标题是「docs: tidy up the contributor notes」,整个 diff 只是在
`CLAUDE.md` 里加了一行。

```
MergeWarden: NEEDS REVIEW

Why: This file can change how AI agents behave in future PRs. (CLAUDE.md)
Next: Review the agent instruction/tooling change before merging.
```

- **这个 PR 有没有越出它自己声明的范围?** 智能体在 PR 正文的契约里声明打算改动的路径,超出范围的改动会被记为检查结果。
- **它有没有动到智能体控制平面?** 对 `AGENTS.md`、`CLAUDE.md`、`GEMINI.md`、`.mcp.json`、`.cursor/**` 这类文件的改动,会左右此后每一个智能体 PR 的行为,理应经过人眼确认。
- **它有没有把不可信文本接进智能体的提示词?** 从 PR 正文、标题或评论流向已登记的 agentic 工作流的新路径,会被追踪并标记出来
[这个 PR](https://github.com/sjh9714/agent-gate-install-smoke-20260617/pull/22)
是公开的,你可以自己把输出和 diff 对一遍。不管开 PR 的是不是智能体它都有效,那一条记录的是
`Agent detected: no`

它同时也会捕捉工作流权限提升、未按 SHA 固定的供应链引用,以及有风险的软件包生命周期脚本。
默认监视:`AGENTS.md`、`CLAUDE.md`、`GEMINI.md`、`QWEN.md`、`.mcp.json`、`.cursor/`、
`.codex/`、`.gemini/`、`.github/copilot-instructions.md`、`claude_desktop_config.json`。

它**不会**执行 PR 中的代码,**不会**从 PR 的 head 读取策略,运行时**不会**调用大模型。每一个判定都附带可在本地复现的确定性证据。
**这种事多常见?** 我们扫了 GitHub 上 2,204 个已合并的智能体 PR,其中 **3.9%** 改动了
这类文件。它不是每天都会发生的事,而是那种你希望在它发生的那一次就知道、而不是六周后才
发现的事。

MergeWarden 也用自己来把关自己的 PR —— 上面那枚 `MergeWarden` 徽章就是这个实时自检([我们如何 dogfooding](docs/demo-prs.md#dogfooding-mergewarden-gates-its-own-prs))。
> 本页其余部分仍是旧版结构的翻译,内容属实但组织方式落后于
> [英文 README](README.md)。以英文版为准。

## 60 秒试一下

Expand Down Expand Up @@ -67,9 +86,6 @@ jobs:
steps:
- uses: sjh9714/mergewarden@v0.10.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
mode: warn
fail-on-block: false
comment: auto
```

Expand Down
16 changes: 8 additions & 8 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,18 @@ jobs:
steps:
- uses: sjh9714/mergewarden@v0.10.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
mode: warn
fail-on-block: false
comment: auto
```

There is no checkout step and no config file to write. MergeWarden reads the
pull request through the GitHub API, and until you add a `mergewarden.yml` of
your own it uses a built-in default policy.
There is no checkout step, no config file to write, and no token to create: the
Action uses the one GitHub already gives the job. MergeWarden reads the pull
request through the GitHub API, and until you add a `mergewarden.yml` of your
own it uses a built-in default policy.

`mode: warn` with `fail-on-block: false` means nothing is ever blocked from
merging. Leave it that way for a while.
`comment: auto` is the only option worth setting on a fresh install. The default
policy already runs in `warn` mode, which never blocks a merge, so
`mode: warn` and `fail-on-block: false` are what you would get anyway. Leave it
that way for a while.

## What the first pull request looks like

Expand Down
3 changes: 0 additions & 3 deletions docs/integrations/copilot.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@ jobs:
steps:
- uses: sjh9714/mergewarden@c32fb900b65708ea7c875b8ec4244c0983343970 # v0.10.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
mode: warn
fail-on-block: false
comment: auto
```

Expand Down
Loading