Know what was grafted onto main.
Coding agents open pull requests faster than people can review them, and a description written by the agent being reviewed is not evidence of anything. Graft is a GitHub App that mounts every pull request like a specimen: who wrote it, what it claims, whether the diff agrees, and what your policy would say.
Phase 0, shadow mode: one managed comment and three check runs per pull request, and every check concludes neutral. Graft reports; it never enforces.
Det. verifier
claude-sonnet-5-5 supported 3 of 3 claims
saw the diff, the file list and the card only
Graft herbarium · acme, fictional
- Authored by
- Agent (Graft trailers)
- Agent
- claude-code
- Model
- claude-sonnet-5-5
- Task
- #128
- Opened by
- acme-agent[bot]
Goal: Limit failed login attempts to 5 per account per 15 minutes and lock the account for 15 minutes after that.
acme organisation, mounted with its provenance label and the verifier's slip.The pull request description comes from the agent being judged. Graft treats it as data, not evidence.
Dozens of agent pull requests a day, each read line by line, each introduced by prose its own author wrote. CODEOWNERS can say who reviews a path. It cannot say that the change touching src/auth/ was written by an agent, under which task, and that it does more than its description admits.
Graft sits on your existing GitHub repositories, with nothing to migrate. It reads the commits and the diff through the GitHub API, writes down what it finds, and leaves the decision with your reviewers.
Four answers, each signed by whoever produced it.
Like a specimen's chain of custody, every answer on the pull request names its source, and a later hand can only add caution, never remove it.
-
Who wrote it?
An agent (which one, which model, which task, which session) or a human. Read from
Graft-Agent,Graft-Model,Graft-TaskandGraft-Sessiongit trailers or the PR description, bot accounts, configured logins and agent co-authors.Signed by the commits · check run
graft/provenance -
What does it claim?
An intent card: the goal in one sentence, the observable behavior changes, and the edits the goal does not explain. A writer model drafts it from the title, description, linked issue and diff; the untrusted PR text is fenced as data.
Signed by the writer model · check run
graft/intent -
Does the diff match?
A second model call, the verifier, sees only the diff, the file list and the card, never the title, description or issue, and checks every claim. Fixed rules set a risk floor that no model can lower.
Signed by the verifier and the rules · in
graft/intent -
What does the policy say?
Your
graft.yaml, read from the base branch only, says whether the change would be auto-approved, need humans or a team, or be blocked. In Phase 0 it reports and decides nothing.Signed by your policy · check run
graft/policy
- GitHubsigned webhook
- Serververifies, deduplicates, enqueues in one transaction
- Postgres 17 + pg-bossdelivery row and job
- Workerre-reads the PR, commits, diff and
graft.yamlfrom the API; calls the writer and the verifier - GitHubthree neutral check runs and one managed comment
The risk floor. Rules set it; a model can only raise it.
Every pull request gets a class: trivial, logic or sensitive. Fixed rules over paths, size, deleted tests, CI, dependency, infrastructure, auth and migration files set the floor. The final class is the highest of the floor and every model opinion. Try it.
Plate IV. A simplified subset of the floor rules in Intent cards, for illustration. Without a checked card (no API key, model down, budget used up) the risk is at least logic.
Policy you can read in one sitting.
graft.yaml lives in the repository and is read from the base branch only, so a pull request cannot rewrite the rules it is judged by. Block wins, then require, then approve; with no match, the default applies.
- A
graft/policycheck and a policy section in the managed comment. pnpm graft policy checkon the command line and as a CI lint.- A JSON Schema for editor completion.
- Report only in Phase 0: the check always concludes
neutral.
# Extra paths that set the intent risk floor to `sensitive`.
version: 1
sensitive_paths:
- services/billing/**
- "**/payments/**"
# Used when no rule matches. Must be a `require`.
default:
require: { humans: 1 }
rules:
- name: docs-only
match: { risk: trivial, only_paths: ["docs/**", "*.md"] }
approve: auto
- name: infra-and-auth
match: { paths: ["infra/**", "**/auth/**"] }
require: { humans: 1, team: security }
- name: agent-sensitive
match: { author: agent, risk: sensitive }
require: { humans: 2 }
- name: no-agent-secrets
match: { paths: ["secrets/**"], author: agent }
block: true
Pressed, not preserved. Graft keeps the label, not the specimen.
Never stored or logged
File contents, diffs, patches, PR titles and bodies. Patches and commit messages are read in memory; only the parsed Graft-* trailers are kept.
Stored
Ids, numbers, SHAs, file paths, counts, and the model's own prose (goal, behavior changes, reasons). Logs carry metadata only: ids, counts, codes.
Sent to Anthropic
To write and verify a card, the diff, the PR text and the linked issue text go to the Anthropic API. Pilot teams agree to this before onboarding. Without an API key, Graft runs with the rule-based floor only.
Kept briefly
Webhook payloads sit in the job queue and are deleted one hour after their job completes or finally fails.
A field note on shadow mode.
Graft is in shadow mode. It reports and never enforces: every Graft check concludes neutral, a "would approve" is not an approval, and team membership is not checked. Keep your branch protection as it is.
Phase 0 ends at a gate measured from data: at least 90% of eligible reviewer votes must say the intent cards are accurate, with enough votes, voters, installations and days behind the number, decided with a Wilson interval. The steps are built; the gate awaits pilot data from two pilot teams.
Phase 1, where the policy check becomes required, is planned. None of it is implemented yet.
The development planRunning offline in under five minutes.
Node 24, pnpm 11 and Docker. Only pnpm install and the image pull touch the network; no GitHub App or API key is needed.
pnpm install
pnpm db:up # Postgres 17 on 127.0.0.1:55432
cp .env.example .env
openssl genrsa -out dev.pem 2048
pnpm db:migrate
pnpm dev:server # terminal 1
pnpm dev:worker # terminal 2
pnpm webhook:send installation.created --delivery smoke-1
202 {"status":"enqueued"}
curl localhost:3000/readyz
{"status":"ready","db":"ok","queue":"ok"}
What a reviewer sees.
An agent adds a login rate limiter; the shipped example policy applies; one reviewer has approved. This is the managed comment as Graft's renderers produce it, for a fictional pull request in a fictional
acmeorganisation.Reviewers rate the card with a reaction or with the buttons on the
graft/intentcheck. Those votes feed the Phase 0 accuracy gate.Checks
graft/provenanceNeutralgraft/intentNeutral Card is accurateCard is wronggraft/policyNeutralGraft
Intent
Goal: Limit failed login attempts to 5 per account per 15 minutes and lock the account for 15 minutes after that.
Risk:
sensitive· rulessensitive· writerlogic· verifierlogicBehavior changes
Out-of-scope edits: none
How this card was made
sensitive: authentication, security or secrets (src/auth/rate-limit.ts,src/auth/login.ts)claude-sonnet-5-5; verifierclaude-sonnet-5-5supported 3 of 3 claimsCard
0b6f3a52· head4c9e1f2· Rate this card with 👍 or 👎 on this comment, or with the buttons on thegraft/intentcheck.Provenance
claude-codeclaude-sonnet-5-5#128acme-agent[bot]Head
4c9e1f2· 2 commits, 2 with Graft trailers.Policy
Would require 2 humans (1 from
@acme/security) · report onlysrc/auth/login.ts,src/auth/rate-limit.ts,test/auth/rate-limit.test.tssensitive(card), author agentclaude-codeApprovals so far: 1 on this head (
dana). Team membership is not checked in Phase 0.Policy
graft.yamlonmainat9d8c7b6· head4c9e1f2· Graft only reports in Phase 0; it approves and blocks nothing.Maintained by Graft; edits to this comment are overwritten.
Plate III. The managed comment, from the sample in the README. Hidden section markers are left out.