Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

208 lines · 13.8 KBRaw History

title: Push protection description: How Diffinery scans git pushes and API file writes for leaked credentials, what a blocked push looks like, and how to opt out or bypass the check.

Push protection scans the files in every git push for credentials such as cloud keys, API tokens and private keys, and rejects the whole push if it finds one. It also scans files written through the REST API's contents and git data endpoints. It is always on for every repository: there is nothing to enable, and nothing from a blocked push or write is stored.

How it works

When you run git push, Diffinery receives your packfile and checks it before it writes anything:

  1. It unpacks the packfile in memory. No object is written to the repository yet.
  2. It scans the new file contents (blobs) in the pack against a fixed list of credential patterns.
  3. If nothing matches, it stores the objects and then applies the usual checks (permissions and branch protection) to each ref you pushed.
  4. If anything matches, it rejects every ref in the push, prints what it found, records the attempt in the audit log and emails you. Nothing from the pack is kept, so a retry starts from a clean state, like git's own object quarantine.

A push is all or nothing: if one commit on one branch contains a secret, every branch and tag in that push is rejected, not only the affected one.

What is detected

Diffinery looks for these credential types. Each name below is exactly what appears in the push output, the audit log and the email.

NameWhat matches
AWS access keyAKIA or ASIA followed by 16 uppercase letters or digits
GitHub tokenghp_, gho_, ghu_, ghs_ or ghr_ followed by 36 letters or digits, or github_pat_ followed by 82 letters, digits or underscores
Diffinery tokendfy_ followed by at least 32 letters, digits, _ or - (a Diffinery personal or agent token)
Slack tokenxoxb-, xoxa-, xoxp-, xoxr- or xoxs- followed by at least 10 letters, digits or -
Stripe live keysk_live_ or rk_live_ followed by at least 20 letters or digits
Google API keyAIza followed by 35 letters, digits, _ or -
Anthropic API keysk-ant- followed by at least 20 letters, digits, _ or -
OpenAI API keysk- or sk-proj- followed by at least 40 letters, digits, _ or -
Private keyA PEM header line: -----BEGIN PRIVATE KEY-----, or the same with RSA, EC, OPENSSH or DSA before PRIVATE KEY, or -----BEGIN PGP PRIVATE KEY BLOCK-----

Each pattern must stand on word boundaries, so a key embedded in a longer word does not match. Some other details:

  • A file is reported at most once per credential type, even if it holds several keys of that type.
  • One value can match more than one type. For example, an Anthropic key also has the shape of an OpenAI key, so the output can list both Anthropic API key and OpenAI API key for the same file.
  • Only live Stripe keys are detected. Test keys (sk_test_...) are not.
  • An encrypted PEM key (-----BEGIN ENCRYPTED PRIVATE KEY-----) is not matched by the Private key pattern.
  • The list is fixed. You cannot add your own patterns.

What is scanned

ScannedNot scanned
Blobs (file contents) contained in the packfile of a git pushCommits, trees and tags themselves (commit messages are not scanned)
In a push, text files smaller than 1 MiB (1,048,576 bytes)In a push, blobs of 1 MiB or more
Every ref in the push: branches and tagsBinary files: any blob that contains a NUL byte anywhere
Files written with PUT /repos/{owner}/{repo}/contents/{path}Files that contain the text diffinery:allow-secret (see Opting a file out)
Blobs created with POST /repos/{owner}/{repo}/git/blobs, and inline content in POST /repos/{owner}/{repo}/git/treesObjects the server already has, which git does not send again
Git LFS objects, which are uploaded separately from the push
Release assets
The web editor, POST /repos/{owner}/{repo}/diffinery/commits and the MCP commit_files tool

Pushes larger than 100 MB are refused before scanning with push exceeds 100 MB; split it into smaller pushes.

Important: Push protection is a safety net, not a guarantee. It finds only the formats listed above, and changes made through the web editor, POST /repos/{owner}/{repo}/diffinery/commits or MCP are not scanned. Never rely on it as your only defense: keep secrets in Actions secrets and out of your repository.

When a push is blocked

What you see

git prints Diffinery's messages with a remote: prefix, followed by a rejection for each ref:

$ git push origin main
Enumerating objects: 5, done.
...
remote: Push blocked: possible secrets detected.
remote:   AWS access key in blob 3f2a9c1d0e
remote:   Private key in blob 8b41e07a5c
remote: Remove the secret from your commits, or rotate it and contact an admin.
To https://diffinery.com/acme/website.git
 ! [remote rejected] main -> main (secret detected (push protection))
error: failed to push some refs to 'https://diffinery.com/acme/website.git'

The format is:

  • The first line is always Push blocked: possible secrets detected.
  • Then one line per finding, <type> in blob <first 10 characters of the blob id>, for at most 10 findings.
  • Then Remove the secret from your commits, or rotate it and contact an admin.
  • If you passed -o allow-secrets=... but are not allowed to bypass, one more line: Only repository admins (people, not agents) can bypass push protection.
  • Every ref in the push is rejected with the reason secret detected (push protection).

What else happens

  • The attempt is recorded in the audit log as push.blocked_secret, with the repository as the target and the detected types and refs as detail. The secret values themselves are never recorded.
  • You get an email with the subject [acme/website] Your push was blocked: possible secrets. If an agent pushed, the email goes to the agent's owner (for an organization's agent, to the organization's owners and admins). This is a security email and is sent whatever your notification settings are.
  • Nothing from the push is stored in the repository.

How to fix it

  1. Find the file. The blob id prefix in the output identifies the file version. In your local clone, run:

    git rev-list --objects --all | grep '^3f2a9c1d0e'

    The line it prints ends with the path of the file.

  2. Remove the secret from every commit you are pushing, not only the latest one. For the most recent commit:

    # edit the file to remove the secret, then
    git add path/to/file
    git commit --amend --no-edit

    If the secret was added in an earlier commit, rewrite those commits with an interactive rebase (git rebase -i origin/main) and edit each one that contains it.

  3. If the value was a real credential, rotate it at its provider. Treat it as exposed: it was on your machine in a commit and may have been pushed elsewhere.

  4. Push again.

Writes through the REST API

The GitHub-compatible file endpoints run the same scan on each file they write:

EndpointWhat is scanned
PUT /repos/{owner}/{repo}/contents/{path}The new file content
POST /repos/{owner}/{repo}/git/blobsThe blob
POST /repos/{owner}/{repo}/git/treesEach tree entry given with inline content; entries given by sha refer to blobs already stored and are not scanned again

The patterns, the binary-file rule and the diffinery:allow-secret opt-out are the same as for pushes. The 1 MiB limit does not apply: every text file is scanned, whatever its size.

When a file matches, the request fails with 422 and nothing is stored:

{
  "message": "push protection: possible secret detected (AWS access key)",
  "documentation_url": "https://docs.github.com/rest"
}

There is no bypass for API writes, not even for repository admins. If the file really must contain the value, add the diffinery:allow-secret marker to it, or push it with git and use the bypass. Each refused write is recorded in the audit log as api.blocked_secret with the repository as the target and the detected types as detail. No email is sent for it.

Opting a file out

Test fixtures sometimes need values that look like real keys. To let such a file through, put the text diffinery:allow-secret anywhere in it, usually in a comment:

// diffinery:allow-secret (fake keys for the parser tests)
export const FIXTURE_KEY = "AKIAIOSFODNN7EXAMPL3";

The marker applies to the whole file, not to one line. If diffinery:allow-secret appears anywhere in a file's content, Diffinery skips every pattern for that entire file. Writing it at the end of a line, as in key = AKIA... # diffinery:allow-secret, works only because it puts the marker somewhere in the file; any other credential in the same file is skipped too.

The opt-out is visible in the code and goes through review like any other change, so use it only for files whose secrets are fake. It matches the exact text diffinery:allow-secret (case-sensitive), in any comment syntax.

Bypassing push protection

A repository admin who is a person can push a flagged change anyway by giving a reason with a git push option:

git push -o allow-secrets="revoked key kept in a migration test" origin main

--push-option=allow-secrets=... is the long form of the same option.

Who can bypass

All of these must be true:

RequirementDetail
You have the admin role on the repositoryThe repository owner, an owner or admin of the organization that owns it, a collaborator with the admin role, or a site admin. See Permissions.
You are a personAgents can never bypass push protection, whatever their role.
Your token allows admingit authenticates with a token. A write-scoped token caps your role at maintain, so you need a token with the admin scope. See Tokens.
You give a reasonThe value after allow-secrets= must not be empty or only spaces. It is trimmed and cut to 200 characters.

If any requirement is missing, the push is blocked as usual. When you supplied the option but are not allowed to use it, the output adds Only repository admins (people, not agents) can bypass push protection.

What a bypass does

  • The push continues and the output shows Push protection bypassed by diffy: <reason>, followed by the findings (up to 10).
  • The bypass skips only the secret scan. Branch protection still applies to every ref.
  • It is recorded in the audit log as push.secret_bypassed, with the detected types, your reason and the refs.
  • An email with the subject [acme/website] Push protection was bypassed goes to you and to the repository's admins (the owner or the organization's owners and admins, plus admin collaborators who are people). It names who pushed, the types found, the reason and the refs. You cannot turn this email off.

Bypassing does not make a real credential safe: anyone who can read the repository can read it. Rotate real credentials even after a bypass.

Differences from GitHub push protection

Diffinery's push protection follows the same idea as GitHub's: scan at push time, block before anything is stored, and allow an audited bypass. The differences:

GitHubDiffinery
PatternsA large catalog of provider patterns, plus custom patternsThe nine types listed above; no custom patterns
EnablingA repository or organization settingAlways on for every repository; cannot be turned off
BypassA link in the push output where you pick a reason, then push againgit push -o allow-secrets=<reason> in the same push; repository admins who are people only
Excluding filesPaths in .github/secret_scanning.ymlA diffinery:allow-secret marker inside each file
NotificationSecret scanning alerts in the repositoryAudit log entries and email to the pusher (and to admins on a bypass)
Existing historySecret scanning can also scan existing contentOnly new blobs in each push (and each file written through the contents and git data APIs) are scanned

Troubleshooting

SymptomCause and fix
secret detected (push protection) on every refAt least one file in the push matched. Read the remote: lines above it for the type and blob, then follow How to fix it.
The push is still blocked after you deleted the file in a new commitThe secret is still in an earlier commit you are pushing. Rewrite the commit that added it instead of adding a commit on top.
Only repository admins (people, not agents) can bypass push protection.You used -o allow-secrets=... without being allowed. Check your role, that you are not using an agent's token, and that your token has the admin scope.
-o allow-secrets= seems to be ignoredThe reason is empty. Give a non-empty reason, for example -o allow-secrets="test fixture".
A real key was not caughtIt is not one of the formats above, it is in a binary file or in a pushed file of 1 MiB or more, the file contains diffinery:allow-secret, or the change was made with the web editor, diffinery/commits or MCP. Rotate the key and remove it.
push protection: possible secret detected (...) (422) from the APIA file you wrote with the contents or git data API matched. Remove the value, or mark a test fixture with diffinery:allow-secret. API writes have no bypass.
push exceeds 100 MB; split it into smaller pushesThe pack is over the push size limit. Push fewer commits at a time, or move large files to Git LFS.