Scanner

Hand-instrumenting every write is the scarcest cost in the funnel. The scanner removes it: point it at a repository and it finds the writes, names the verbs, and writes the gates.

./writ scan ./my-repo

What it finds

The scanner parses Python with the AST (no regexes, no LLM) and flags calls that write to a system of record:

  • Database — execute, session.add, session.commit, save, insert, delete (on db-ish receivers)
  • HTTP — requests.post, client.put, session.patch
  • Files — open(..., "w"), Path.write_text, os.remove, shutil.rmtree
  • Email — sendmail, send_message
  • Queues — publish, produce, enqueue
  • AWS — put_object, put_item, delete_object, send_message

Read queries are not flagged: SELECT/WITH statements are skipped. Test files, tests/ directories, virtualenvs, and hidden directories are skipped by default.

Verbs

Each write maps to a verb in <domain>.<action> form. The domain comes from the file path (src/payments/refunds.py → payments); the action from the function name through a synonym table (issue_refund → refund, create_order → create, notify_customer → send).

verbs discovered: 6
orders.create
orders.delete
orders.send
payments.create
payments.refund

The three outputs

  1. Policy — writ-policy.json maps every discovered verb to require_grant, the gate’s default for unknown verbs. Review it, then --push-policy --key writ_KEY to PUT it directly.
  2. Instrumentation diff — a unified diff inserting a _writ_check gate at the top of each uninstrumented function, plus a small stdlib-only helper module block per file. The helper reads WRIT_API_KEY, WRIT_SPONSOR, WRIT_AGENT, and WRIT_BASE from the environment and fails closed (DENY) on any error.
  3. Coverage report — write sites found vs. already gated, overall and per file. Re-run after applying to watch it hit 100%.
writ scan: /repo
files scanned: 24 skipped: 0
write sites: 31 in 12 function(s)
gated: 4/31 (12%)

Apply with approval

The scanner never writes without asking. --apply prompts per run; --yes skips the prompt for CI. Functions the scanner can’t safely gate (one-liners, module-level writes) are listed as manual for you to handle by hand.

./writ scan ./my-repo --apply
# apply 9 gate(s) to 2 file(s)? [y/N]

The generated code calls POST /v1/check with your verb before the write runs. ALLOW proceeds; anything else raises PermissionError before the write happens — so the receipt exists even for denied writes.