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.
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).
The three outputs
- Policy —
writ-policy.jsonmaps every discovered verb torequire_grant, the gate’s default for unknown verbs. Review it, then--push-policy --key writ_KEYto PUT it directly. - Instrumentation diff — a unified diff inserting a
_writ_checkgate at the top of each uninstrumented function, plus a small stdlib-only helper module block per file. The helper readsWRIT_API_KEY,WRIT_SPONSOR,WRIT_AGENT, andWRIT_BASEfrom the environment and fails closed (DENY) on any error. - Coverage report — write sites found vs. already gated, overall and per file. Re-run after applying to watch it hit 100%.
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.
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.