https://github.com/megabyte0x/safegit
Safe-backed Git approval CLI, API, and GitHub required-check workflow
https://github.com/megabyte0x/safegit
eip-712 erc-1271 git github-actions multisig safe viem
Last synced: 14 days ago
JSON representation
Safe-backed Git approval CLI, API, and GitHub required-check workflow
- Host: GitHub
- URL: https://github.com/megabyte0x/safegit
- Owner: megabyte0x
- License: mit
- Created: 2026-05-17T19:16:39.000Z (about 2 months ago)
- Default Branch: main
- Last Pushed: 2026-05-21T08:31:47.000Z (about 1 month ago)
- Last Synced: 2026-05-21T14:09:21.459Z (about 1 month ago)
- Topics: eip-712, erc-1271, git, github-actions, multisig, safe, viem
- Language: JavaScript
- Homepage: https://agent-cortex.github.io/safegit-site/
- Size: 67.4 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
- Security: SECURITY.md
Awesome Lists containing this project
README
# SafeGit
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 `Verified` badge. 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.
## What is included
- Git wrapper CLI: `safegit add`, `commit`, `status`, `push`, and other Git commands pass through to `git`
- SafeGit overlay commands: `safegit setup`, `sign`, `approval status`, `verify`, plus legacy `request` and `attest`
- 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 Verified` required check
- pnpm-only package management
## Architecture
```text
Developer runs safegit as a git wrapper
-> safegit add / commit delegate to git
-> safegit sign creates a commit approval request
-> Postgres shared approval state
-> SafeGit API + approval page
-> Safe owners sign EIP-712 typed data
-> safegit push verifies approval, then delegates to git push
-> GitHub Action can also run safegit verify
```
## Requirements
- 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
## Install locally
SafeGit is pnpm-only.
Run these commands from the SafeGit CLI package directory, the directory that contains `package.json` and `bin/safegit.js`.
```bash
pnpm install
```
Check the local entrypoints before linking globally:
```bash
pnpm safegit -- --help
pnpm run safegit:server -- --help
```
Link the CLI globally when you want to run `safegit init` from arbitrary target repos:
```bash
pnpm link:global
hash -r
```
Check the global shims:
```bash
safegit --help
safegit-server --help
```
If `safegit migrate` or `safegit init` fails with `Cannot find module '.../bin/safegit.js'`, the global shim was linked from the wrong directory or an older checkout. Re-run `pnpm link:global` from the CLI package directory and clear the shell command cache with `hash -r`. On pnpm 11+, the underlying command is `pnpm link --global .`; the trailing `.` is required.
Without a global link, use the bin path directly from any target repo:
```bash
node /absolute/path/to/safegit/bin/safegit.js init --safe 0xYourSafeAddress --chain-id 11155111 --threshold 2
```
## Run local API + Postgres
```bash
docker compose up --build
```
The API starts at:
```text
http://127.0.0.1:8787
```
Postgres is published on the host at `127.0.0.1:15432` by default so it does not collide with an existing developer-machine Postgres on `5432`. Override the host port with `SAFEGIT_POSTGRES_HOST_PORT` before running Docker Compose.
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:
```bash
safegit env
safegit doctor
```
This creates `.env` in the current directory with the Docker Compose database URL, `postgres://safegit:safegit_dev_password@127.0.0.1:15432/safegit`, if `SAFEGIT_DATABASE_URL` is missing. `safegit doctor` validates the URL, DNS, and TCP connectivity. Use `safegit env --database-url 'postgres://USER:PASSWORD@HOST:5432/DB'` for a hosted or manually managed database. If an existing `.env` contains a stale or malformed value, use `safegit env --force`.
Optional live Safe validation:
```bash
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.
## Exact usage flow
### 1. Prepare the database
If you are using Docker Compose, the API does this automatically. If you are using a manually managed database, run:
```bash
safegit env --database-url 'postgres://USER:PASSWORD@HOST:5432/DB'
safegit doctor
safegit migrate
```
### 2. Configure SafeGit inside the target Git repo
```bash
cd /path/to/your/repo
safegit env
safegit setup \
--safe 0xYourSafeAddress \
--chain-id 11155111 \
--threshold 2
```
This writes `.safegit.yml` and stores the repo → Safe mapping in Postgres.
`safegit init --safe ...` remains as a legacy alias for setup. Plain `safegit init` delegates to `git init`.
### 3. Stage and commit exactly like Git
```bash
safegit add path/to/file
safegit commit -m "describe the protected change"
```
`safegit add`, `safegit commit`, `safegit status`, and other Git-shaped commands pass their original arguments to `git`. SafeGit does not reimplement Git's index, pathspec, hooks, editor, or transport behavior.
### 4. Create an approval request for the commit
```bash
safegit sign
```
This creates a `GitCommitApproval` EIP-712 payload for `HEAD` 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` and `approvalUrl` like:
```text
{
"approvalId": "appr_5f4074bfbf53",
"approvalUrl": "http://127.0.0.1:8787/approve/appr_5f4074bfbf53"
}
```
The legacy `safegit request --ref HEAD` command is still available, but `safegit sign` is the normal wrapper flow.
### 5. Open the approval page
```text
http://127.0.0.1:8787/approve/
```
The page:
1. loads the backend-provided EIP-712 payload
2. connects an injected wallet
3. calls `eth_signTypedData_v4`
4. posts `{ signer, signature }` to the SafeGit API
5. marks the approval `approved` after the configured threshold is reached
### 6. Check approval status
```bash
safegit approval status --ref HEAD
```
`safegit status` delegates to `git status`.
Before enough signatures:
```json
{
"status": "pending"
}
```
After threshold:
```json
{
"status": "approved"
}
```
### 7. Push through the SafeGit wrapper
```bash
safegit push
```
`safegit push` first checks that `HEAD` has an approved SafeGit approval. If approval is missing or pending, it exits before running `git push`. Once approved, it delegates to `git push` with the original arguments.
### 8. Enforce approval in CI
```bash
safegit verify --ref HEAD
```
`verify` exits non-zero unless the commit is approved. Use that as a GitHub required check.
## Manual signature submission
If you collect a signature elsewhere, submit it manually:
```bash
safegit attest \
--approval-id appr_xxxxxx \
--signer 0xSignerAddress \
--signature 0xSignatureBytes
```
Or through the API:
```bash
curl -X POST http://127.0.0.1:8787/api/approvals/appr_xxxxxx/signatures \
-H 'content-type: application/json' \
-d '{
"signer": "0xSignerAddress",
"signature": "0xSignatureBytes"
}'
```
## API endpoints
```text
GET /healthz
GET /approve/:approvalId
GET /api/approvals/:approvalId
POST /api/approvals/:approvalId/signatures
```
## Production hardening
SafeGit's approval page is intentionally small, but a public deployment should still sit behind basic controls.
### API bearer token
Set `SAFEGIT_API_TOKEN` to require `Authorization: Bearer ` on all `/api/*` routes:
```bash
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:
```text
https://safegit.example.com/approve/#token=
```
Do not put this token in GitHub Actions logs, public issues, or screenshots.
### Rate limits and CORS
By default, `/api/*` routes are limited to 60 requests per minute per IP. Tune with:
```bash
export SAFEGIT_RATE_LIMIT_WINDOW_MS=60000
export SAFEGIT_RATE_LIMIT_MAX=60
```
Lock CORS to your deployed origin instead of `*`:
```bash
export SAFEGIT_CORS_ORIGIN='https://safegit.example.com'
```
### Reverse proxy
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:
```bash
export SAFEGIT_TRUST_PROXY=true
```
This makes per-IP rate limiting use the forwarded client IP instead of the proxy's IP.
### Live Safe validation
Set `SAFEGIT_RPC_URL` so submitted signatures are checked against live `getOwners()` / `getThreshold()` before they count:
```bash
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.
### GitHub App roadmap
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.
## GitHub Actions required check
The repo includes:
```text
action.yml
templates/github-action.yml
```
Example workflow:
```yaml
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: HEAD
```
Then require the `Safe Verified` check in GitHub branch protection.
## What Postgres stores
- `safegit_repos` — repo slug, host, owner, repo name, Safe address, chain ID, threshold
- `safegit_approval_requests` — commit SHA, branch, EIP-712 payload, message hash, status, expiry
- `safegit_signatures` — signer address, signature, timestamp
## Testing
```bash
pnpm install --frozen-lockfile
pnpm test
pnpm pack --dry-run
```
## GitHub App status
A 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.
## Security notes
- Do not commit private keys, RPC secrets, database passwords, or `.env` files.
- 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_URL` if you want live Safe owner/threshold validation.
- For rigorous historical validation, verify the Safe owner set at the relevant block.
## License
MIT