> Call POST /v1/check before a tool that writes. DENY means do not invoke the tool. For clean Markdown of any page, append .md to the page URL. For the complete index, fetch https://docs.withwrit.com/llms.txt.

# CLI reference

> Every ./writ command, grouped by job.

The `writ` CLI wraps the API. Install it:

#### pip

```bash
pip install pywrit
```

#### curl

```bash
curl -fsSL https://withwrit.com/writ -o writ
chmod +x writ
```

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

## Keys, login, and config

```bash
./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`.

```bash
./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

```bash
./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

```bash
./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](/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](/scanner) for
the full story.

Every scan report ends with a pointer to the Writ GitHub App (install:
[https://github.com/apps/writ-scanner](https://github.com/apps/writ-scanner), guide:
[https://docs.withwrit.com/git-hub-app](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

```bash
# 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

```bash
./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

```bash
./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

```bash
./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

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

## API keys, badge, vault, stats

```bash
./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

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

## GitHub App

```bash
./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

```bash
./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

```yaml
- 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

```bash
./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.
```

```bash
./writ explain rcpt_xxx
# Human-readable breakdown of a receipt.
```

```bash
./writ completion bash >> ~/.bashrc
./writ completion zsh >> ~/.zshrc
# Source the generated completion script. Reload your shell to enable it.
```

## Kill switch

```bash
./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

```bash
./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

```bash
# 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](/audit-log#agent-action-report) for what the report
aggregates and how the money totals are derived.