Skip to main content
Glama

pywrit

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.

Listed on mcpservers.org

writ scan finds 4 write sites in a Python agent (0/4 gated); writ scan --apply inserts gates; a re-scan shows 4/4 gated

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.

Install

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.

Related MCP server: Vorim AI — Agent Identity & Trust

Language coverage

Language

writ scan

writ scan --apply

Notes

Python

Yes

Yes, inserts _writ_check gates

Parsed with the stdlib ast, no extra needed

TypeScript / JavaScript

Yes, with pywrit[polyglot]

No, scan only

Needs the extra: pip install 'pywrit[polyglot]'. Without it, the report says so loudly: a warning block names the unscanned files instead of reporting zero findings

Go

Yes, with pywrit[polyglot]

No, scan only

First detection family plus request-pipeline detection: database/sql Exec/ExecContext, GORM Create/Save/Update/Updates/Delete, HTTP client writes and http.NewRequest with a write method, generated-client ..<New|Update|Delete|Create>, request-pipeline X.Call/X.RawRequest with a write method; package-level generated clients not yet covered. Without the extra, the report names the unscanned Go files in the same warning block as TS/JS

60-second quickstart: scan → wrap → gate

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

What writ scan detects

Python (.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

Email

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, ...

  • Each write is mapped to a verb such as payments.refund or crm.update, inferred from the file path and function name.

  • A function that already calls writ_check(...) / _writ_check(...) counts as gated.

  • Skipped: tests, hidden directories, virtualenvs, node_modules, dist, build. Use --exclude SUBSTR (repeatable) to skip more.

  • Not covered (review by hand): writes behind dynamically built SQL, third-party SDK calls such as stripe.Refund.create(...), deferred task queues, or shared clients several layers down. The risk report lists these gaps every time.

TypeScript / JavaScript (scan-only)

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 prominent warning block naming the unscanned TS/JS files and keeps going — Python scanning never needs it.

Findings from non-product paths (docs/, scripts/, sandbox/, examples/, benchmark/, demo/, and build-tooling files such as gulpfile.js or webpack.config.*) are suppressed by default: the report counts them per class and names every suppressed path instead of listing them as findings. Pass --include-non-product to report them like any other finding.

Every scan report ends with a one-line pointer to the Writ GitHub App (install: https://github.com/apps/writ-scanner, guide: https://docs.withwrit.com/git-hub-app), which runs the scan on every push and pull request; the JSON report (--format json) carries the same pointer under github_app.

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, JS SDK calls such as stripe.refunds.create(...), and generated REST resource clients: create / update / patch / remove methods (plus their ...WithHttpInfo variants) that build request options with an explicit write HTTP method and route them through the client request pipeline (twilio-style generated clients, e.g. twilio-node). (Still not covered: the Python SDK equivalents like stripe.Refund.create(...) — see "Not covered" above.)

writ scan on a TypeScript agent finds 5 write sites, 1 of 5 gated

Go (scan-only)

writ scan also finds write sites in Go. It needs the optional extra (tree-sitter based):

pip install 'pywrit[polyglot]'

Covered in this first family plus request-pipeline detection: database/sql Exec / ExecContext, GORM Create / Save / Update / Updates / Delete, HTTP client Post / Put / Patch / Delete and http.NewRequest / NewRequestWithContext with a write method, generated-client calls shaped <client>.<Resource>.<New|Update|Delete|Create> with an uppercase resource segment (the stripe-go shape), and request-pipeline calls X.Call(method, ...) / X.RawRequest(method, ...) where argument 0 is a write method (the stripe.Backend shape, e.g. c.B.Call(http.MethodPost, "/v1/accounts", ...)). GET/HEAD requests, db.Query / QueryRow, bare constructors, and pipeline calls with a variable method or a non-method first argument are not flagged. _test.go files and vendor/ trees are skipped. Go is scan-only: writ scan --apply never rewrites .go files, gate those by hand. Not yet covered: package-level generated clients (charge.New(...), the pre-v87 stripe-go shape), GORM raw-SQL helpers, and other Go write patterns.

GitHub Action

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

GitHub App

Install the Writ GitHub App on selected repositories: every push and pull request is scanned for ungated write-sites, with results as a check run on the PR and in the portal, where you manage policy, keys, and the audit log. Setup and data handling: GitHub App docs.

MCP server (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 17 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

writ_github_installations

List connected GitHub App installations

writ_github_set_threshold

Set the check-run fail threshold per install

writ_github_toggle_pr_comments

Enable/disable the PR-comment bot

writ_manifests

List repos with risk profiles or fetch one manifest

writ_explain_receipt

Explain a receipt in plain language

writ_tenant_notify

Get or update step-up notification settings

writ_principals

List the tenant's principals

writ_verbs

List the tenant's verb vocabulary

writ_keys

List API key metadata

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 and listed on Glama.

Python client

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 time

  • verify_token(...): validate an ALLOW auth token (catches purpose drift)

  • grant(...): human-sponsor approval for the STEP_UP path

  • get_policy() / set_policy(...): manage the tenant policy

  • revoke(...) / reinstate(...) / revoked(): the kill switch

  • receipts() / receipt(id) / verify_chain() / stream_receipts(): the audit log

  • sandbox(...): keyless trial, no API key required

CLI reference

writ 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.

README badge

Show that your agent's writes are gated by Writ. Two flavors:

Static (works today, no API key) — same design, no live count:

[![agent writes gated by Writ](https://cdn.jsdelivr.net/gh/withwrit/pywrit@main/badge/writ-gated.svg)](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).

Docs

Full docs: docs.withwrit.com · Quickstart: docs.withwrit.com/quickstart · Site: withwrit.com

License

MIT

Available Tools

19 tools
writ_checkA

Ask Writ whether an action may proceed. CALL THIS BEFORE any consequential write.

A consequential write is anything hard to undo: sending money or messages, changing access or identity records, deleting data, calling an external API that acts in the world.

Args: sponsor_id: The human sponsor accountable for this action (e.g. a user id or email). agent_id: The agent or workflow performing the action. verb: What is being done (e.g. "verify_human", "send_payment"). target: What it acts on (e.g. "benefit-case-123"). purpose: Why, in plain words. Be specific — the approval is bound to this purpose. environment: Optional policy environment: "dev", "staging", or "prod". The check runs against that environment's policy override (verbs it doesn't override inherit the tenant default). Omit for the tenant default. An unknown name denies fail-closed — never silently falls back.

Returns a decision: ALLOW — proceed. You get an authToken valid 90 seconds, bound to this exact sponsor/agent/verb/target/purpose. Call writ_verify_token against the intended write immediately before executing it. DENY — do NOT perform the action. Explain the receipt reason to the user. STEP_UP — a human sponsor must approve first. Tell the user what needs approval; they can approve via writ_grant (or the dashboard), then call writ_check again. Every outcome creates an audit receipt with a receiptId.

Inputs: verb (required), target (required), purpose (required), agent_id (required), +2 more. Returns: result

writ_executeA

Run one gated upstream request with a lease or ALLOW token.

Pass exactly one of lease_id (from writ_lease) or auth_token (from a writ_check ALLOW). Pass the SAME method, path, and body as the lease: the gate compares the request bytes against the lease, and on any mismatch it fails closed: no upstream call is made and the answer is {"ok": false, "reason": ..., "upstreamCalled": false}.

Args: lease_id: The leaseId from writ_lease. Mutually exclusive with auth_token. auth_token: An ALLOW authToken from writ_check. Mutually exclusive with lease_id. system_id: The registered upstream system: "stripe" (default), "github", or "salesforce". method: HTTP method of the upstream request (required, e.g. "POST"). path: Upstream path (required, e.g. "/v1/refunds"). body: Optional JSON object. Must be byte-identical to the leased body. idempotency_key: Optional key passed through to the upstream system. Use it so retries are safe.

On success the gate returns the upstream system's status and body verbatim. Every outcome writes a receipt. On {"ok": false} report the reason to the user and do NOT retry with a different request: the lease is spent or invalid.

Inputs: body, path, method, lease_id, +3 more. Returns: result

writ_explain_receiptA

Fetch a receipt and explain it in plain language.

Use this to understand why a writ_check returned ALLOW, DENY, STEP_UP, or REVOKED. Returns the receipt with a human-readable breakdown.

Inputs: receipt_id (required). Returns: result

writ_github_installationsA

List the GitHub App installations for this tenant.

Returns each installation's id, status, PR-comment bot setting, and the per-installation check-run fail threshold (checkFailUngated). Use this to discover installation ids before calling writ_github_set_threshold or writ_github_toggle_pr_comments.

Inputs: no named parameters. Returns: result

writ_github_set_thresholdA

Set the per-installation check-run fail threshold.

A positive number N makes the GitHub check run fail when the ungated-site count is greater than N; a zero-ungated scan passes. Omit threshold (or set it to None) to clear the override and restore neutral-only check runs.

Inputs: installation_id (required), threshold. Returns: result

writ_github_toggle_pr_commentsA

Enable or disable the optional PR-comment bot for a GitHub App installation.

Default is OFF. No TOTP step-up is required. Use writ_github_installations to find installation_id.

Inputs: enabled (required), installation_id (required). Returns: result

writ_grantA

SPONSOR ONLY. Mint a one-time grant approving a specific action.

Use this when writ_check returned STEP_UP and the human sponsor approves. The grant is single-use: the next writ_check consumes it and returns ALLOW. The grant is scoped to your tenant. Requires WRIT_SPONSOR_TOKEN.

Inputs: verb (required), target (required), purpose (required), agent_id (required), +2 more. Returns: result

writ_keysA

List API key metadata for the tenant (never raw keys).

Inputs: no named parameters. Returns: result

writ_leaseA

Convert an ALLOW token into a single-use gated-execution lease.

Call this AFTER writ_check returns ALLOW, passing that ALLOW's authToken. The lease is bound to the exact upstream request you intend to run: the same method, path, and body bytes you will later pass to writ_execute. A lease is single-use; once executed (or expired) it is spent.

Args: auth_token: The authToken from a writ_check ALLOW decision. system_id: The registered upstream system: "stripe", "github", or "salesforce". A vaulted credential must be active for this system or the gate answers ok:false. method: HTTP method of the upstream request (e.g. "POST"). path: Upstream path (e.g. "/v1/refunds"). Must match the system's allowed path patterns. body: Optional JSON object (at most 1 MB). Sent only when provided; if you lease a body you must execute the identical body. ttl_seconds: Optional lease TTL in seconds. The gate caps it at 90 and at the token's remaining life.

On success the gate answers 201 with leaseId, expiresAt, ttlSeconds, verb, target, purpose, and systemId. On failure it answers {"ok": false, "reason": ...} (token invalid or expired, or no active vaulted credential for the system). On ok:false do NOT execute.

Inputs: path (required), method (required), system_id (required), auth_token (required), +2 more. Returns: result

writ_manifestsA

List scanned repos and their risk profiles, or fetch one manifest.

With no repo_id, returns a table of repos with writScore, scanStatus, and riskDigest (counts per risk class). With repo_id, returns the full signed manifest for that repo.

Inputs: repo_id. Returns: result

writ_policyA

Read the tenant's verb policy: which verbs allow, deny, require a grant, or step up.

Inputs: no named parameters. Returns: result

writ_principalsB

List the tenant's principals (identity directory).

Inputs: no named parameters. Returns: result

writ_receiptsB

List recent decision receipts (audit trail). Newest first.

Inputs: limit. Returns: result

writ_reinstateA

SPONSOR ONLY. Undo writ_revoke: the principal is evaluated by policy again.

Scoped to your tenant. Requires WRIT_SPONSOR_TOKEN.

Inputs: agent_id (required), sponsor_id (required). Returns: result

writ_revokeA

SPONSOR ONLY. Kill switch: immediately deny all future checks for this principal.

Revocation overrides live grants and tenant policy. Use when a principal is compromised, misbehaving, or its task is done. Scoped to your tenant. Requires WRIT_SPONSOR_TOKEN.

Inputs: agent_id (required), sponsor_id (required). Returns: result

writ_sandboxB

Get a free 90-second sandbox grant for a demo write. No API key needed.

Issues a live grant for verb "demo_write" on a target starting with "demo-", then runs writ_check so you get back an ALLOW decision plus auth token to practice the verify-before-write flow end to end.

Inputs: target (required), purpose (required), agent_id (required), sponsor_id (required). Returns: result

writ_tenant_notifyB

Get or update step-up notification settings.

With no arguments, returns the current settings. With arguments, updates them: step_up_email enables/disables step-up emails; step_up_webhook_url sets a generic or Slack webhook URL; use an empty string to clear.

Inputs: step_up_email, step_up_webhook_url, step_up_webhook_type. Returns: result

writ_verbsA

List the tenant's verb vocabulary, including custom verbs.

Inputs: no named parameters. Returns: result

writ_verify_tokenA

Verify a Writ auth token against the write you are about to execute.

Call this IMMEDIATELY before executing an ALLOWed write, with the exact verb/target/purpose you intend. If valid is false — expired, tampered, or bound to a different purpose — do NOT execute the write; call writ_check again.

This is the point-of-action enforcement: the check authorizes, the token proves the authorization still matches what you are actually doing.

Inputs: verb (required), target (required), purpose (required), auth_token (required). Returns: result

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.6/5.0

    Scored across 19 tools

    Disambiguation4/5

    The core governance pipeline (writ_check → writ_lease → writ_execute → writ_verify_token) is well differentiated by explicit ordering cues and mutually exclusive arguments, and the GitHub/admin tools each target a distinct resource. Minor overlap remains: writ_lease and writ_execute both accept an authToken and near-identical method/path/body arguments, and writ_receipts vs writ_explain_receipt could be momentarily confused (list vs. explain one).

    Naming Consistency4/5

    All 19 tools share a consistent writ_ prefix and snake_case, and most read as verb_noun (writ_check, writ_revoke, writ_github_set_threshold). The deviation is a cluster of noun-only names for list operations (writ_receipts, writ_policy, writ_manifests, writ_principals, writ_verbs, writ_keys), which is readable but breaks the verb-first pattern.

    Tool Count4/5

    19 tools is slightly above the ideal band but each maps to a real step in the authorize/lease/execute/audit lifecycle or a concrete GitHub/admin operation. It tips toward heavy because of five-plus read-only listing tools (principals, verbs, keys, manifests, receipts) that could arguably be consolidated.

    Completeness4/5

    The authorization lifecycle is complete: check, lease, execute, point-of-action verify, step-up grant, revoke/reinstate, plus audit receipts and a sandbox demo path. Gaps: policy and API keys are read-only (no policy write or key create/rotate tools), and GitHub PR-comment/threshold settings are covered while broader installation administration is not.

    Maintenance

    ActivityActive
    ResponsivenessLimited data

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      C
      maintenance
      AI agent identity, permissions, trust scores, and tamper-evident audit trails. 17 MCP tools: register agents (Ed25519 keypairs), check permissions (sub-5ms), emit audit events, verify trust scores (0-100), delegate credentials, ephemeral agents. IETF Internet-Draft filed. Works with LangChain, OpenAI, CrewAI, Stripe ACP. npx @vorim/mcp-server
      17
      83 npm
      66
      MIT
    • A
      license
      B
      quality
      B
      maintenance
      A governance proxy for AI tools — every MCP/agent tool call is policy-gated, secret-redacted, and written to a hash-chained, offline-verifiable audit trail.
      13
      MIT
    • A
      license
      A
      quality
      A
      maintenance
      Gates every AI agent tool call against your policy before it runs, then writes a hash-chained, tamper-evident record of what was decided and what happened. Anyone can verify that record offline, without your software, your systems or any key you hold.
      4
      147 PyPI
      15
      AGPL 3.0