Graft

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.

A pull request drawn as a graft The rootstock stem is the main branch with three commits. A scion, the branch feat/login-rate-limit with two commits, is set into it at an angle and bound with tape at the graft union. a41c0d2 e07b9f1 9d8c7b6 · base rootstock · main c51e7a0 4c9e1f2 · head scion · feat/ login-rate-limit graft union

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.

sensitiverules floor
Plate I. A fictional pull request in a fictional 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.

  1. Who wrote it?

    An agent (which one, which model, which task, which session) or a human. Read from Graft-Agent, Graft-Model, Graft-Task and Graft-Session git trailers or the PR description, bot accounts, configured logins and agent co-authors.

    Signed by the commits · check run graft/provenance

  2. 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

  3. 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

  4. 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

  1. GitHubsigned webhook
  2. Serververifies, deduplicates, enqueues in one transaction
  3. Postgres 17 + pg-bossdelivery row and job
  4. Workerre-reads the PR, commits, diff and graft.yaml from the API; calls the writer and the verifier
  5. GitHubthree neutral check runs and one managed comment
Plate II. The worker never trusts the webhook payload: it re-reads everything from the GitHub API and publishes under a per-change lock. Architecture

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 acme organisation.

Reviewers rate the card with a reaction or with the buttons on the graft/intent check. Those votes feed the Phase 0 accuracy gate.

Checks

  • graft/provenanceNeutral
  • graft/intentNeutral Card is accurateCard is wrong
  • graft/policyNeutral
Graftmanaged comment · fictional example

Graft

Intent

Goal: Limit failed login attempts to 5 per account per 15 minutes and lock the account for 15 minutes after that.

Risk: sensitive · rules sensitive · writer logic · verifier logic

Behavior changes

  • The sixth failed login for an account within 15 minutes returns 429 with a Retry-After header.
  • A successful login resets the failure counter for that account.

Out-of-scope edits: none

How this card was made
  • Rules set the floor to sensitive: authentication, security or secrets (src/auth/rate-limit.ts, src/auth/login.ts)
  • Writer claude-sonnet-5-5; verifier claude-sonnet-5-5 supported 3 of 3 claims
  • Inputs: PR title and description, issue #128, 3 of 3 files in full
  • Model usage: 7,421 input tokens, 913 output tokens, 2 calls

Card 0b6f3a52 · head 4c9e1f2 · Rate this card with 👍 or 👎 on this comment, or with the buttons on the graft/intent check.

Provenance

Authored byAgent (Graft trailers)
Agentclaude-code
Modelclaude-sonnet-5-5
Task#128
Sessionagents.example.com
Opened byacme-agent[bot]

Head 4c9e1f2 · 2 commits, 2 with Graft trailers.

Policy

Would require 2 humans (1 from @acme/security) · report only

  • Rule 3 infra-and-auth: paths match src/auth/login.ts, src/auth/rate-limit.ts, test/auth/rate-limit.test.ts
  • Rule 4 agent-sensitive: risk sensitive (card), author agent claude-code

Approvals so far: 1 on this head (dana). Team membership is not checked in Phase 0.

Policy graft.yaml on main at 9d8c7b6 · head 4c9e1f2 · 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.

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.

Files in the pull request (fictional)
What the models say
trivialfinal Floor trivial: nothing in the rules matched.

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/policy check and a policy section in the managed comment.
  • pnpm graft policy check on the command line and as a CI lint.
  • A JSON Schema for editor completion.
  • Report only in Phase 0: the check always concludes neutral.
The full graft.yaml reference
graft.yamldocs/examples, abridged
# 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
Plate V. Five of the example policy's eight rules. In Plate III, infra-and-auth and agent-sensitive both matched, and the comment reports what they require together: 2 humans, 1 from security.

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.

What Graft reads, stores and sends, table by table

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 plan

Running 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.

Getting started, step by step
shell
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"}

Read the card before you read the diff.