SafeGit makes protected Git changes require Safe multisig approval.
It creates an EIP-712 GitCommitApproval payload for a commit, collects Safe owner signatures, stores approval state in Postgres, and lets CI enforce a Safe Verified required check before merge.
SafeGit does not try to fake GitHub's native
Verifiedbadge. GitHub's badge supports GPG, SSH, and S/MIME signatures; Safe smart accounts validate signatures through Safe owner signatures / ERC-1271 patterns. SafeGit uses a GitHub required-check flow instead.
- CLI:
safegit migrate,init,request,attest,status,verify - HTTP API for approval retrieval and signature submission
- Built-in browser approval page at
/approve/:approvalId - Postgres shared state for repos, approval requests, and signatures
- EIP-712 typed-data payload generation
- Signature recovery with
viem.recoverTypedDataAddress - Optional live Safe
getOwners()/getThreshold()validation via RPC - Docker Compose for local API + Postgres
- GitHub Action template for a
Safe Verifiedrequired check - pnpm-only package management
Developer CLI
-> Postgres shared approval state
-> SafeGit API + approval page
-> Safe owners sign EIP-712 typed data
-> optional live Safe owner/threshold validation
-> GitHub Action runs safegit verify
-> branch protection requires Safe Verified
- Node.js 22+
- pnpm 11+
- Git
- Postgres 17+ or Docker Compose
- A Safe address and threshold
- Optional but recommended: RPC URL for the Safe's chain
SafeGit is pnpm-only.
pnpm install
pnpm link --globalCheck the CLI:
safegit --help
safegit-server --helpdocker compose up --buildThe API starts at:
http://127.0.0.1:8787
The API server runs migrations automatically on startup. Use safegit migrate directly for manual setup, hosted Postgres, CI/CD migrations, or future schema upgrades.
For CLI commands in another shell:
export SAFEGIT_DATABASE_URL='postgres://safegit:safegit_dev_password@127.0.0.1:5432/safegit'Optional live Safe validation:
export SAFEGIT_RPC_URL='https://ethereum-sepolia-rpc.publicnode.com'When SAFEGIT_RPC_URL is set on the API server, submitted signatures are checked against the live Safe owner set and threshold.
If you are using Docker Compose, the API does this automatically. If you are using a manually managed database, run:
export SAFEGIT_DATABASE_URL='postgres://USER:PASSWORD@HOST:5432/DB'
safegit migratecd /path/to/your/repo
safegit init \
--safe 0xYourSafeAddress \
--chain-id 11155111 \
--threshold 2This writes .safegit.yml and stores the repo → Safe mapping in Postgres.
safegit request --ref HEADThis creates a GitCommitApproval EIP-712 payload containing:
- repo host, owner, and name
- branch
- commit SHA
- tree SHA and parent SHAs
- author and committer
- Safe address
- chain ID
- approval ID
- creation and expiry timestamps
The command prints an approvalId like:
appr_5f4074bfbf53
http://127.0.0.1:8787/approve/<approvalId>
The page:
- loads the backend-provided EIP-712 payload
- connects an injected wallet
- calls
eth_signTypedData_v4 - posts
{ signer, signature }to the SafeGit API - marks the approval
approvedafter the configured threshold is reached
safegit status --ref HEADBefore enough signatures:
{
"status": "pending"
}After threshold:
{
"status": "approved"
}safegit verify --ref HEADverify exits non-zero unless the commit is approved. Use that as a GitHub required check.
If you collect a signature elsewhere, submit it manually:
safegit attest \
--approval-id appr_xxxxxx \
--signer 0xSignerAddress \
--signature 0xSignatureBytesOr through the API:
curl -X POST http://127.0.0.1:8787/api/approvals/appr_xxxxxx/signatures \
-H 'content-type: application/json' \
-d '{
"signer": "0xSignerAddress",
"signature": "0xSignatureBytes"
}'GET /healthz
GET /approve/:approvalId
GET /api/approvals/:approvalId
POST /api/approvals/:approvalId/signatures
SafeGit's approval page is intentionally small, but a public deployment should still sit behind basic controls.
Set SAFEGIT_API_TOKEN to require Authorization: Bearer <token> on all /api/* routes:
export SAFEGIT_API_TOKEN='replace-with-a-long-random-token'The browser approval page still renders at /approve/:approvalId. If API auth is enabled, signers can either paste the token into the page once or open:
https://safegit.example.com/approve/<approvalId>#token=<token>
Do not put this token in GitHub Actions logs, public issues, or screenshots.
By default, /api/* routes are limited to 60 requests per minute per IP. Tune with:
export SAFEGIT_RATE_LIMIT_WINDOW_MS=60000
export SAFEGIT_RATE_LIMIT_MAX=60Lock CORS to your deployed origin instead of *:
export SAFEGIT_CORS_ORIGIN='https://safegit.example.com'For production, terminate HTTPS at a reverse proxy such as Caddy, Nginx, Cloudflare Tunnel, Fly, Render, or a platform load balancer. Keep Postgres private, rotate SAFEGIT_API_TOKEN, and enable platform-level request logging/rate limits too. If the service is behind one trusted proxy, set:
export SAFEGIT_TRUST_PROXY=trueThis makes per-IP rate limiting use the forwarded client IP instead of the proxy's IP.
Set SAFEGIT_RPC_URL so submitted signatures are checked against live getOwners() / getThreshold() before they count:
export SAFEGIT_RPC_URL='https://ethereum-sepolia-rpc.publicnode.com'For rigorous historical validation, use an archival RPC and verify owner/threshold state at the relevant chain block for the protected change. The MVP validates current Safe ownership; that is enough for demos and most active approvals, not for backdated forensic guarantees.
The MVP uses a GitHub Action required check because it is simple and works today. A future GitHub App should add webhook-triggered check-runs, centralized repo configuration, audit pages, and secretless installation UX.
The repo includes:
action.yml
templates/github-action.yml
Example workflow:
name: Safe Verified
on: [pull_request]
jobs:
safe-verified:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: agent-cortex/safegit@main
with:
database-url: ${{ secrets.SAFEGIT_DATABASE_URL }}
ref: HEADThen require the Safe Verified check in GitHub branch protection.
safegit_repos— repo slug, host, owner, repo name, Safe address, chain ID, thresholdsafegit_approval_requests— commit SHA, branch, EIP-712 payload, message hash, status, expirysafegit_signatures— signer address, signature, timestamp
pnpm install --frozen-lockfile
pnpm test
pnpm pack --dry-runA GitHub App is not required to test SafeGit. The current MVP uses a GitHub Action required check.
A future GitHub App would improve production UX by creating check-runs directly from push/PR webhooks, centralizing repo configuration, and avoiding manual workflow setup.
- Do not commit private keys, RPC secrets, database passwords, or
.envfiles. - Use a real shared Postgres database for team usage; local DBs are only for demos.
- For production, run the API behind auth/rate limits.
- Enable
SAFEGIT_RPC_URLif you want live Safe owner/threshold validation. - For rigorous historical validation, verify the Safe owner set at the relevant block.
MIT