> Call POST /v1/check before a tool that writes. DENY means do not invoke the tool. For clean Markdown of any page, append .md to the page URL. For the complete index, fetch https://docs.withwrit.com/llms.txt.

# Quickstart

> From zero to your first gated write in minutes.

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

```bash
pip install pywrit
```

Alternative: a standalone script you can run without pip.

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

## 2. Get a key

```bash
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](/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.

```bash
# 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:

|                 | DENY                           | ALLOW                               |
| --------------- | ------------------------------ | ----------------------------------- |
| `decision`      | `"DENY"`                       | `"ALLOW"`                           |
| Why (this demo) | `require_grant`, no live grant | sandbox grant matched and spent     |
| `receiptId`     | yes                            | yes                                 |
| `authToken`     | no                             | 90s, bound to this write            |
| Next step       | do **not** write               | write 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](/concepts) for the modes (`allow` | `deny`
\| `require_grant` | `step_up`) and [Audit log](/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:

```python
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:

```bash
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](/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

* [Concepts](/concepts) — principals, decisions, receipts
* [CLI reference](/cli-reference) — every `./writ` command
* [API reference](/api-reference) — the REST API