Skip to navigation

Quickstart

One path, end to end: install the CLI, get a key, prove the gate (DENY → sandbox → ALLOW), wrap your first endpoint, then point the scanner at your own codebase.

Repo script for the proof below: ./scripts/demo-five-minute.sh (dry-runs without a key — prints the commands and hits the keyless sandbox).

1. Install

pip install pywrit

Alternative: a standalone script you can run without pip.

curl -fsSL https://withwrit.com/writ -o writ
chmod +x writ
./writ scan .

2. Get a key

writ key --email you@company.com
export WRIT_API_KEY=writ_...

The sponsor token (writ_sp_...) is shown once for a new tenant. Save it in a password manager — it approves grants and revokes principals, and it is never recoverable. See Authentication for the full two-credential model.

3. Prove the gate

Keep sponsor, agent, verb, target, purpose identical across sandbox and check — the grant matches on all five fields.

# DENY — demo_write defaults to require_grant; no live grant exists.
writ check \
--key "$WRIT_API_KEY" \
--sponsor you --agent demo-agent \
--verb demo_write --target demo-1 \
--purpose "quickstart proof"
# → "decision": "DENY" + receiptId. Do not run the write.
# Sandbox grant — keyless, 90s, demo_write on demo-* targets only.
writ sandbox \
--sponsor you --agent demo-agent \
--target demo-1 --purpose "quickstart proof"
# → grantId + expiresAt
# ALLOW — the grant matched and was spent (one-shot).
writ check \
--key "$WRIT_API_KEY" \
--sponsor you --agent demo-agent \
--verb demo_write --target demo-1 \
--purpose "quickstart proof"
# → "decision": "ALLOW" + authToken + receiptId
# Receipts + chain
writ receipts --key "$WRIT_API_KEY"
writ verify-chain --key "$WRIT_API_KEY"
# → {"ok": true, "checked": N, ...}

What just happened:

DENYALLOW
decision"DENY""ALLOW"
Why (this demo)require_grant, no live grantsandbox grant matched and spent
receiptIdyesyes
authTokenno90s, bound to this write
Next stepdo not writewrite once; optional token-verify

A second identical check after ALLOW is DENY again — the grant was spent. Real verbs use writ grant --sponsor-token ... (or the dashboard). See Concepts for the modes (allow | deny | require_grant | step_up) and Audit log for receipts and chain verification.

Two CLI notes: --key is required on each subcommand (check / receipts / verify-chain do not read WRIT_API_KEY), and output is raw JSON — parse decision, receiptId, ok.

4. Wrap your first endpoint

Add one check in front of the write:

from pywrit.client import WritClient
client = WritClient(api_key="writ_...", base_url="https://api.withwrit.com")
@app.post("/refunds")
def refund(order_id: str):
result = client.check(
sponsor_id="you",
agent_id="api",
verb="payments.refund",
target=order_id,
purpose="refund order",
)
if result.decision != "ALLOW":
raise HTTPException(403, "Denied by Writ")
# ... your refund logic

Every decision writes a receipt to your tamper-evident audit log. To revoke the principal later — one call, no key rotation, no redeploy:

writ revoke --sponsor YOU --agent YOU --reason "rogue" --sponsor-token $WRIT_SPONSOR_TOKEN

5. Scan your codebase

Have an existing codebase? Give this prompt to your coding agent to instrument it. The scanner is an on-ramp — discovery, not complete security coverage. See Scanner.

Instrument this Python repository with Writ. Writ puts an allow/deny
gate in front of every function that writes to a system of record —
database, API, file, email, queue, or AWS call.
Best for: Python agent scripts and services that call Stripe, HTTP APIs,
or databases. Coverage: Python: scan + auto-instrument.
TypeScript/JavaScript: scan-only (pip install 'pywrit[polyglot]',
or use the GitHub App where the grammars are bundled). Go: scan-only
(first detection family). --apply rewrites Python files only.
Do not modify production data, deploy, rotate credentials, push policy,
or use production keys. Work in the repository root only; do not scan
parent directories or monorepo siblings.
1. Install and scan
From the repository root, run:
pip install pywrit
writ scan .
Writ is a Python on-ramp: it finds likely writes (database, HTTP, file,
email, queue, AWS) and suggests verb names like
<domain>.<action> — the domain comes from the directory/file/class, the
action from the function name (e.g. payments/refunds.py + issue_refund()
becomes payments.refund).
One check is added per function (not per write site). A plain scan
writes writ-policy.json into the repo and shows an instrumentation
diff, but it does not modify your source files. Only --apply edits
source files (it asks for approval first). Every scan also prints
a risk report: a 0-100 score grading each discovered write
HIGH / MEDIUM / LOW (`writ scan --score` prints the report alone;
it is fully read-only and writes nothing).
Scan is discovery, not complete security coverage. Known gaps:
raw SQL strings built dynamically, boto3 wrappers that rename API
calls, ORM relationship writes, and shared HTTP clients three layers
down may be missed. (ORM .save()/.delete() instance calls and Celery
task definitions are detected.) Note any you find as coverage gaps.
2. Review the policy
Read writ-policy.json. For each verb, confirm:
- The domain matches the system of record (payments, orders, users…)
- The action matches the intent (refund, create, delete…)
- No write is missing from the list
To exclude paths, use --exclude with a path substring.
New verbs default to require_grant mode: the check will DENY until a
sponsor grants it, or you change the policy to allow. For a sandbox
demo, set money verbs to require_grant and everything else to allow.
3. Apply instrumentation
After human review and approval, run:
writ scan . --apply
This inserts a _writ_check() helper at module level and adds a gate at
the top of each function. ALLOW proceeds, DENY raises PermissionError.
The helper is idempotent — re-running scan will not double-instrument.
Every decision writes a tamper-evident receipt to your audit log, but
only if WRIT_API_KEY is set. Without a key, checks fail closed to DENY
and no receipt is written.
4. Verify
Set WRIT_API_KEY, then:
- Run one call that should be DENIED. Confirm no write happens and
PermissionError is raised.
- Run one call that should be ALLOWED. Confirm the write happens and
a receipt appears in the audit log with a valid chain hash.
Report: the verbs instrumented, any coverage gaps, and the exact
commands run. Stop and wait for approval before pushing policy
to production.

Next steps