Commit-time policy checks for AI agent writes (ALLOW/DENY/STEP_UP) with a tamper-evident audit log.
Runs on your machine from its package; the steps are below.

Not assessed yet.
Odel charges nothing per call. The publisher may charge for its own service.
Runs locally over stdio
This server isn't hosted — your MCP client launches it from a package registry. Use one of the commands below, or drop the config into your client (e.g. Claude Desktop).
Run
uvx writ-mcp
MCP client config
{
"mcpServers": {
"writ": {
"command": "uvx",
"args": [
"writ-mcp"
]
}
}
}Find the dangerous writes in your Python agent's code, then gate it. pywrit is the Python
client and writ CLI for Writ: an allow/deny gate that sits
in front of your agent's consequential writes (database, HTTP, files, email, queues,
AWS) and records a hash-chained receipt for every decision.

writ scan is local, deterministic, and free: it parses your code with Python's
ast module, makes no network calls, and needs no API key.
pip install pywrit
Requires Python 3.9+. Installs the writ command and the pywrit Python client.
JavaScript/TypeScript devs: run the scanner with no Python setup via npm:
npx writ-scan .
The writ-scan package wraps the same scanner (including TS/JS support). On
first run it installs pywrit[polyglot] from PyPI using your Python 3.9+
(one-time); afterwards it starts instantly. See npm/ for details,
environment overrides (WRIT_PYTHON, WRIT_SCAN_NO_INSTALL), and the
install test.
1. Scan. See which functions write, and how many of them are gated.
writ scan .
writ scan: /path/to/support-agent
files scanned: 4 skipped: 0
write sites: 4 in 3 function(s)
gated: 0/4 (0%)
verbs discovered: 3
crm.update
payments.refund
tickets.close
It also prints a risk report (0-100, weighted by risk tier), writes the discovered
verbs to writ-policy.json, and shows the instrumentation it would add as a unified
diff. Nothing in your code changes yet. writ scan . --score prints just the risk report.
2. Wrap. One command takes the repo from unwrapped to wrapped: scan, show the diff, ask for explicit approval, apply the gates, and report before/after coverage.
writ wrap . # interactive approval
writ wrap . --yes # no prompt (e.g. in CI)
--dry-run previews the plan and exits before prompting; --diff-only prints the
unified diff for review tools. Each gated function now asks Writ before it writes, and
fails closed:
def issue_refund(charge_id, amount_cents):
if _writ_check("payments.refund") != "ALLOW":
raise PermissionError("writ denied payments.refund")
...
writ scan . --apply is still available as the manual alternative. Re-run writ scan .
and you'll see gated: 4/4 (100%).
3. Gate. Get a free API key, load the discovered policy, and run your agent.
writ key --email you@example.com # free API key + tenant
export WRIT_API_KEY=writ_... # read by the inserted gate
export WRIT_SPONSOR=acme WRIT_AGENT=support-agent # optional: who is acting
writ scan . --push-policy --key "$WRIT_API_KEY" # upload the discovered verb policy
Every gated write now gets ALLOW, DENY, or STEP_UP (a human sponsor must
approve), and every decision is written to your tenant's tamper-evident audit log:
writ receipts --key "$WRIT_API_KEY" # latest receipts
writ verify-chain --key "$WRIT_API_KEY" # verify the receipt hash chain
writ stream --key "$WRIT_API_KEY" # tail decisions live
writ report --key "$WRIT_API_KEY" # Agent Action Report
writ scan detectsPython (.py) files, parsed with the stdlib ast (no extra dependencies):
| Category | Examples |
|---|---|
| Database | cursor.execute(...) / executemany / executescript (write SQL only; SELECT/WITH/... skipped), session.add / commit / delete / merge / flush |
| HTTP | requests.post / put / patch, client.delete(...), session.request(...) |
| Files | open(..., "w"/"a"/"x"/"+"), Path.write_text / write_bytes / unlink / rename, os.remove / rename / makedirs, shutil.rmtree / move / copy |
sendmail, send_message, send_email | |
| Queues | publish, produce, enqueue, queue.send(...) |
| AWS SDK | put_object, put_item, delete_item, upload_file, send_message, start_execution, ... |
payments.refund or crm.update, inferred
from the file path and function name.writ_check(...) / _writ_check(...) counts as gated.node_modules, dist, build.
Use --exclude SUBSTR (repeatable) to skip more.stripe.Refund.create(...), deferred task queues, or shared clients
several layers down. The risk report lists these gaps every time.writ scan also finds write sites in TypeScript and JavaScript. It needs the
optional extra (tree-sitter based):
pip install 'pywrit[polyglot]'
writ scan .
Without the extra, the scanner prints a one-line hint and keeps going — Python scanning never needs it.
TS/JS is scan-only: findings are listed with verbs for your policy file,
but writ scan --apply never rewrites TS/JS files — gate those by hand.
Covered patterns: fetch/axios writes, fs writes, SQL through knex-style
clients, Prisma writes, and JS SDK calls such as stripe.refunds.create(...).
(Still not covered: the Python SDK equivalents like stripe.Refund.create(...)
— see "Not covered" above.)

Scan every pull request for ungated write sites. Findings land as check
annotations on the exact file and line, plus a Markdown risk report in the
job summary. The check fails when an ungated finding meets your
fail-on-risk threshold (default: high).
- uses: actions/checkout@v4
- uses: withwrit/pywrit@v0
with:
fail-on-risk: high # high | medium | low | never
No API key needed. Full reference: docs/github-action.md · example workflow: examples/github-action/writ-scan.yml
writ-mcp)Give any MCP-compatible agent point-of-action policy checks. writ-mcp is a
Model Context Protocol server (stdio transport,
built on the MCP Python SDK) that exposes the Writ gate as 8 MCP tools:
the agent calls writ_check before a consequential write and gets back ALLOW,
DENY, or STEP_UP — with a tamper-evident, hash-chained audit receipt for
every decision.
uvx writ-mcp # no install — runs on demand
# or
pip install writ-mcp # then run `writ-mcp`
| Tool | What it does |
|---|---|
writ_check | The gate: ALLOW / DENY / STEP_UP for a proposed write |
writ_verify_token | Validate an ALLOW auth token (catches purpose drift) |
writ_grant | Human-sponsor approval for the STEP_UP path |
writ_revoke / writ_reinstate | The kill switch |
writ_receipts | Read the tenant's audit log |
writ_policy | Manage the verb policy |
writ_sandbox | Keyless 90-second demo grant — no API key needed |
The gate tools need a free Writ API key (WRIT_API_KEY): 10,000 receipts/month
free, no credit card. writ_sandbox works with no key at all.
The server implementation lives in mcp/ (MIT). Also published on the
official MCP Registry as io.github.withwrit/writ, on
Smithery, and listed on
Glama.
from pywrit import WritClient
client = WritClient(api_key="writ_...")
result = client.check(
sponsor_id="acme",
agent_id="agent-7",
verb="db.write",
target="prod.customers",
purpose="backfill region field",
)
if result.decision == "ALLOW":
# result.auth_token is a short-lived token bound to this exact write
perform_write(...)
elif result.decision == "STEP_UP":
# a human sponsor must approve first: client.grant(...), then re-check
...
else:
# DENY
...
No API key yet? Try the keyless sandbox:
client = WritClient()
client.sandbox({
"sponsorId": "acme",
"agentId": "agent-7",
"verb": "demo_write", # sandbox only allows demo_write ...
"target": "demo-customers", # ... on targets starting with demo-
"purpose": "trying the gate",
})
What's covered:
check(...): the gate. ALLOW / DENY / STEP_UP, plus a receipt every timeverify_token(...): validate an ALLOW auth token (catches purpose drift)grant(...): human-sponsor approval for the STEP_UP pathget_policy() / set_policy(...): manage the tenant policyrevoke(...) / reinstate(...) / revoked(): the kill switchreceipts() / receipt(id) / verify_chain() / stream_receipts(): the audit logsandbox(...): keyless trial, no API key requiredwrit wrap . # scan, diff, approve, apply, verify coverage
writ wrap . --dry-run # preview only
writ wrap . --yes # non-interactive CI mode
writ check --key writ_... --sponsor acme --agent agent-7 \
--verb db.write --target prod.customers --purpose "backfill region field"
writ policy --key writ_... --set payments.refund require_grant
writ revoke --sponsor acme --agent agent-7 --reason "runaway loop" # kill switch (sponsor token)
writ grant --sponsor acme --agent agent-7 --verb payments.refund \
--target ch_123 --purpose "approved refund" # STEP_UP approval (sponsor token)
Sponsor-token commands (revoke, reinstate, revoked, grant) read
--sponsor-token or WRIT_SPONSOR_TOKEN. Run writ --help for the full command list.
Show that your agent's writes are gated by Writ. Two flavors:
Static (works today, no API key) — same design, no live count:
[](https://withwrit.com)
Dynamic (live 30-day gated-write count from your audit log) — mint a badge token, then embed the snippet the API returns:
curl -s https://api.withwrit.com/v1/badge/tokens \
-H "Authorization: Bearer writ_..." \
-H "Content-Type: application/json" \
-d '{}'
The dynamic endpoint ships with the gate — until it's live, POST /v1/badge/tokens
returns 404 and the static badge above is the one to use.
See badge/ for embed docs and the verification model (what the
badge proves — and what it doesn't).
Full docs: docs.withwrit.com · Quickstart: docs.withwrit.com/quickstart · Site: withwrit.com
MIT