Skip to navigation

CLI reference

The writ CLI wraps the API. Install it:

pip install pywrit

Set WRIT_BASE to point at a different gate, WRIT_SPONSOR_TOKEN to avoid passing --sponsor-token every time.

Keys, login, and config

./writ login --email you@company.com
# Create a tenant, get an API key + sponsor token, and save both to
# ~/.writ/config.json. After login, --key and --sponsor-token are optional.
./writ key --email you@company.com
# Same as login, but prints credentials instead of saving them.
./writ config show
# Show the current ~/.writ/config.json contents.
./writ config set baseUrl http://localhost:8787
# Point the CLI at a self-hosted gate (also controlled by WRIT_BASE).
./writ config remove apiKey
# Remove a value from config. Environment variables still work.

Credentials are resolved in this order: explicit flag > environment variable (WRIT_API_KEY, WRIT_SPONSOR_TOKEN, WRIT_BASE) > ~/.writ/config.json.

./writ key --email tm@company.com --invite-code writ_inv_...
# Join a teammate's tenant. You get your own API key.
./writ invite --key writ_KEY
# Create a team invite code for your tenant.
./writ me --key writ_KEY
# Account, usage, and policy summary.

Policy

./writ policy --key writ_KEY
# View the tenant's verb policy.
./writ policy --key writ_KEY --set refund step_up
# Modes: allow | deny | require_grant | step_up. Takes effect immediately.
./writ policy --key writ_KEY --env dev
# View the dev environment's override (verbs it explicitly sets).
./writ policy --key writ_KEY --env dev --set refund allow
# Set refund to allow in dev only; other verbs keep inheriting the default.

Repo scanner

./writ scan ./my-repo
# Find likely writes, map to verbs, print the instrumentation diff,
# write writ-policy.json, and report coverage. Changes nothing.
./writ scan ./my-repo --apply
# Prompt, then write the gates into the repo.
./writ scan ./my-repo --apply --yes --push-policy --key writ_KEY
# Apply without prompting and PUT the discovered policy to the gate.
# New verbs default to require_grant, matching the gate default.
./writ scan ./my-repo --prefer-custom-verbs --key writ_KEY
# Fetch the tenant's verb manifest and map inferred verbs onto custom
# verbs when the action matches exactly one. See [Scanner](scanner.mdx).
./writ scan ./my-repo --score
# Read-only risk report (no files changed, no prompts).
./writ scan ./my-repo --exclude tests --exclude migrations
# Skip paths whose relative path contains the given substring.
./writ scan ./my-repo --include-non-product
# Also report TS/JS write sites from non-product paths (docs/, scripts/,
# sandbox/, examples/, benchmark/, demo/, build tooling) that the scan
# suppresses by default and reports as a per-class count.
./writ scan ./my-repo --policy-out verbs.json
# Write the discovered verb set to a different file (default: writ-policy.json).
./writ scan ./my-repo --manifest-out manifest.json --repo-id myrepo \
--commit-sha $(git rev-parse HEAD) --manifest-key "$WRIT_MANIFEST_KEY"
# Emit a signed CI attestation manifest. WRIT_MANIFEST_KEY is also read from
# the environment if --manifest-key is omitted.

The scanner parses Python with the AST: ORM and DB calls, HTTP clients and third-party SDKs (Stripe, Twilio, Resend, SendGrid), file writes, emails, queues, deferred tasks (Celery/RQ/Dramatiq), agent tool registries (LangChain/CrewAI/OpenAI), and AWS SDK writes generated from the botocore service models. SELECT queries are not flagged. Verbs are inferred as <domain>.<action> from the file path and function name (issue_refund in payments/ becomes payments.refund). The scan report also prints a risk profile (db×2, money×1, fs×0, ...) and top ungated sites so you can triage before applying gates. Test files and virtualenvs are skipped.

TS/JS findings from non-product paths — docs/, scripts/, sandbox/, examples/, benchmark/ and demo/ directories, plus build-tooling files (gulpfile.js, Gruntfile.js, webpack.config.*, rollup.config.*, vite.config.*, and equivalents) — are suppressed from the report by default so first scans show product code, not docs and build tooling. Nothing is dropped silently: the report prints the suppressed count per path class and names every suppressed file, and the signed manifest records the same breakdown under findings.tsjs.suppressedNonProduct. --include-non-product reports them as normal findings again. See Scanner for the full story.

Without the polyglot extra, writ scan does not silently skip TS/JS: the report prints a prominent warning block naming the unscanned file count and the exact install command (pip install 'pywrit[polyglot]'), in the normal report and in writ scan --score, and the signed manifest records the state under findings.tsjs.notScanned (reason, extra, files, installCommand).

Go is scan-only too, behind the same extra: a first detection family (database/sql Exec / ExecContext, GORM Create / Save / Update / Updates / Delete, HTTP client writes and http.NewRequest with a write method, generated-client <client>.<Resource>.<New|Update|Delete|Create>), reported as go files scanned / go write sites lines and under findings.go in the signed manifest. _test.go files and vendor/ trees are skipped. Without the extra, Go files are counted in the same warning block (recorded under findings.go.notScanned), and writ scan --apply never rewrites .go files. Package-level generated clients (charge.New(...), the pre-v87 stripe-go shape) are not detected yet. See Scanner for the full story.

Every scan report ends with a 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 with check runs and history in the portal; the signed manifest carries the same pointer under githubApp.

CI attestation

# Fetch the tenant's CI manifest HMAC key (store it in CI as WRIT_MANIFEST_KEY).
./writ manifest-key
# List submitted manifests and risk profiles.
./writ manifests list
# Fetch a repo's full signed manifest and drift.
./writ manifests get myrepo
# Submit a signed manifest produced by `writ scan --manifest-out`.
./writ manifests push manifest.json
# Scan, sign, write locally, and submit in one step.
./writ scan ./my-repo --manifest-out manifest.json --push-manifest --key writ_KEY \
--repo-id myrepo --commit-sha $(git rev-parse HEAD)

The decision loop

./writ check --key writ_KEY --sponsor S --agent A --verb refund --target t-1 --purpose "why"
# ALLOW mints a 90-second token; DENY refuses; STEP_UP needs a grant.
./writ check --key writ_KEY --sponsor S --agent A --verb refund --target t-1 --purpose "why" --environment prod
# Evaluate against the prod policy override (unknown names deny fail-closed).
./writ sandbox --sponsor S --agent A --target demo-1 --purpose "try the gate"
# Free 90-second demo grant for demo_write on demo-* targets. No key needed.
./writ token-verify --key writ_KEY --token writ_at_... --verb refund --target t-1 --purpose "why"
# Prove the ALLOW token matches the write you actually performed.
./writ grant --sponsor S --agent A --verb refund --target t-1 --purpose "why" --sponsor-token TOKEN
# Sponsor approves a STEP_UP. One-time, 90 seconds, exact fields.
# Use --ttl to set a custom lifetime (1–3600 seconds).

Identity directory

./writ principals list
./writ principals create ops
./writ principals rename P_ID billing
./writ principals disable P_ID
./writ principals enable P_ID
./writ principals delete P_ID --force
./writ principals revoke-keys P_ID

Custom verbs

./writ verbs list
./writ verbs add refund --description "Issue a refund"
./writ verbs rename refund --new-name refund_v2
./writ verbs retire refund_v2

Per-principal policy overrides

./writ policy --principal P_ID --set refund allow
./writ policy --principal P_ID --env dev --set refund allow

API keys, badge, vault, stats

./writ keys list
./writ keys create --label "ci-runner" --principal-id P_ID --totp-code 123456
./writ keys revoke KEY_ID
./writ badge create
./writ badge revoke TOKEN
./writ vault list
./writ stats
# 24-hour decision counters.

Gated execution

./writ lease --verb stripe.charge --target cus_123 --purpose "monthly invoice" --ttl 60
./writ execute LEASE_ID

GitHub App

./writ github installations
# List connected installations, PR-comment status, and fail thresholds.
./writ github repos
# List repos and latest scan status.
./writ github repo owner/repo
# Fetch the latest scan findings for one repo.
./writ github threshold INSTALLATION_ID 5
# Set the check-run fail threshold to 5 ungated sites; omit the number to clear.
./writ github pr-comments INSTALLATION_ID on
./writ github pr-comments INSTALLATION_ID off
# Toggle the optional PR-comment bot.
./writ github rescan owner/repo --totp-code 123456
# Queue a fresh scan of a repo (TOTP step-up required).
./writ github disconnect INSTALLATION_ID --totp-code 123456
# Disconnect an installation (TOTP step-up required).

Doctor, templates, and MCP setup

./writ doctor
# Validate config, env vars, and connectivity to the gate.
./writ policy --template prod-safe
./writ policy --env dev --template dev-smooth
# Apply a starter policy template by name (audit-first, prod-safe,
# dev-smooth, strict-ai-defaults) or by path.
./writ mcp-setup --key writ_KEY
# Write Claude Desktop MCP config. Use --cursor for Cursor, or both.

GitHub Action

- uses: AvenueDAdmin/writ@main
with:
api-key: ${{ secrets.WRIT_API_KEY }}
manifest-key: ${{ secrets.WRIT_MANIFEST_KEY }}

This installs pywrit, scans the repo, and submits a signed manifest on every push.

Step-up notifications, explain, and completions

./writ notify
# Show current step-up notification settings.
./writ notify --email off
# Disable step-up emails (webhooks still fire if configured).
./writ notify --webhook https://example.com/writ-hook
# POST generic JSON for every STEP_UP.
./writ notify --slack-webhook https://hooks.slack.com/services/xxx
# POST Slack block-kit messages for every STEP_UP.
./writ notify --remove-webhook
# Clear the webhook.
./writ explain rcpt_xxx
# Human-readable breakdown of a receipt.
./writ completion bash >> ~/.bashrc
./writ completion zsh >> ~/.zshrc
# Source the generated completion script. Reload your shell to enable it.

Kill switch

./writ revoke --sponsor S --agent A --reason "rogue" --sponsor-token TOKEN
# Every later check for this principal is DENY. Writes a receipt.
./writ revoked --sponsor-token TOKEN
# List revoked principals in this tenant.
./writ reinstate --sponsor S --agent A --sponsor-token TOKEN
# Undo a revocation.

Audit log

./writ receipts --key writ_KEY
# This tenant's receipts. Never another tenant's.
./writ stream --key writ_KEY
# Tail the live audit log (SSE). Reconnects automatically.
./writ verify-chain --key writ_KEY
# Verify the receipt hash chain, newest first. See Audit log for what it proves.
# Use --limit N to check the most recent N receipts (default 200).

Agent Action Report

# One-page summary from this tenant's receipts: actions attempted/allowed/denied,
# money totals for money verbs, revocations, per-verb breakdown.
./writ report --key writ_KEY
# Machine-readable, one agent, one window.
./writ report --key writ_KEY --format json --agent refund-agent --since 2026-09-01
# Offline: read receipts from a local file (JSON array or JSON-lines).
./writ report --file receipts.json --period "September 2026"

See Audit log for what the report aggregates and how the money totals are derived.