Cannot touch your CI or workflows
GitHub rejects workflow pushes server-side from any app without the Workflows permission. We never request it.
This page doubles as our GitHub App registration spec: the permission set below is final and frozen. Adding any permission later would force a re-consent screen on every installation. We treat that as a breaking change, because it is one.
The MergePress GitHub App requests exactly three permissions. That’s the whole list, and none of them are org-level, though whether a repo admin can complete the install without an org owner is your organisation’s third-party-access policy, not ours. We just don’t ask for anything org-wide.
Read is repo-wide; writes are not. GitHub does not let an app scope Contents read to a folder, and we use that read deliberately: onboarding walks your file tree to work out your framework and where your blog lives. The confinement below is a write guarantee. If a secret is committed in plaintext in your repo, our read token can see it like any other collaborator, so don’t commit secrets, to us or to anyone.
enforced in the PHP by InstallationAuth::TOKEN_SCOPES, a closed table of three permission sets. A token is minted from one of its entries or from nothing, and an unrecognised scope throws rather than falling back to something wider.
the install screen, verbatim
frozen scope set| Permission | Level | Why |
|---|---|---|
| Contents | Read & write | Read your site to learn its structure and voice; create branches and commits for proposed posts. |
| Pull requests | Read & write | Open the PR, read your review, merge only after you approve. |
| Metadata | Read | Mandatory GitHub baseline: repo name, default branch. |
Enforced by GitHub, not by our promises. These aren’t policies we follow; they’re operations the platform rejects because we don’t hold the permissions they require.
GitHub rejects workflow pushes server-side from any app without the Workflows permission. We never request it.
Separate permissions we don’t hold. Nothing to leak, because nothing is reachable.
Our own broker refuses it. Every change is a PR you can read line by line.
No deploy credentials, for any platform. Publishing is your approval merging the PR.
reference: dependency bots like Renovate must request the Workflows permission to do their job. a content service never needs it, and we never will.
Two fences, one audit trail: GitHub’s server-side enforcement outside, our broker’s hard rules inside.
enforced in the PHP by GitBroker::merge and WritePaths
On a mergepress/* branch, committed via GitHub’s API as mergepress[bot] with GitHub’s Verified signature badge. We never impersonate humans, and we never push to your default branch, the branch prefix is re-checked at merge time, not only when the branch is created, so the guarantee does not lean on GitHub’s ref validation alone.
Your one-tap email approval, your PR review, or you merging the PR yourself. The approval record (who, when, via which channel) is kept as an audit trail, and it is the only thing that unlocks a merge. An approval revoked between granting it and the merge loses: the check runs again as the merge is attempted, not once when the approval arrived.
Set when we first scan your repo: your blog/content path (e.g. src/content/blog/), your framework’s public or static directory for images and the IndexNow key, a gbp/ folder if you use Local, and .mergepress/. Everything else is refused, .github/ and .git/ outright, and so are absolute paths, .., backslashes and null bytes. If we cannot identify your framework we add no public directory at all and the broker refuses the write rather than guessing.
Each job gets an installation token scoped to the single repo and to the minimal permission set for that job (read, propose, or merge), expiring within one hour, which is GitHub’s own maximum. We hold a minted token in memory for its lifetime rather than re-minting per call, and never write one down.
Before any token is served from that cache, or minted at all, we check that your installation is still active, and a no both refuses and drops everything cached for it. That is a status check rather than a reaction to GitHub’s uninstall webhook on purpose: a webhook can be missed, delayed or replayed, and this has to hold when it never arrives.
The writing system is architecturally separated from the system that holds credentials. The model that drafts your content cannot see, hold, or use a GitHub token, a non-AI broker performs every git operation under hard rules.
The app’s private key travels base64-encoded in the platform’s secret manager, is decoded in memory, and is used only to sign short-lived app JWTs. It is never written to disk and never transmitted anywhere, so there is no key file to leave behind on a snapshot. Rotating it is a secret update and a redeploy.
Applies only if you enable the Google Business Profile rail. Your customers’ reviews are their words on Google’s platform. We treat them as something we borrow, not something we keep.
When a reply needs drafting, we fetch the review from Google, use it, and drop it. Review text is never written to our database. Every draft, route, and publish re-fetches the live review, so we hold your customers’ words for seconds, not months.
We store only the sanctioned identifiers (the review’s resource name, our own draft) plus a single review-derived field: the star rating, used to route critical reviews to you. It is purged when the reply reaches a terminal state or at 30 days, whichever comes first, and that erasure runs nightly whether or not the rail is switched on, because an obligation we took on when we stored the field does not pause because a credential expired.
Replies publish through Google’s own API and pass through Google’s moderation. We don’t assume a publish worked; we track the moderation outcome and tell you if Google held or removed a reply.
and the approval rule holds here too: a reply to a one- or two-star review can never publish unattended; the reply gate refuses it on every path, whatever the consent says, and the rail refuses to start at all if it is configured to allow one.
One click in GitHub settings and our GitHub access is gone: GitHub invalidates the installation and its tokens, our own server marks the account deleted and refuses to act for it, and any token still sitting in our in-memory cache is refused and discarded the next time anything asks for it. No commits, no PRs, no merges, ever again.
Two honest footnotes. Google is a separate door: if you enabled Local, you invited our Google identity as a manager on your Business Profile, and uninstalling the GitHub App does not remove it; you revoke that yourself in your profile’s user settings, and we remind you when you cancel. And we keep our own records, not your content: your repository already holds everything we wrote, while on our side we retain the operational log (which cycles ran, which approvals you gave, when), because that trail is the record of your own sign-offs.
Everything we produce lands in your repository. Cancel any time; every post, image, and config file is already yours, in your git history, portable to any other workflow.
Questions about the model? We answer them plainly.