# usage

Source: https://permdock.com/docs/cli/usage

Report permissions that are defined but never used, used but never granted or granted by no role, conditions on undeclared fields, and client checks outside a snapshot include.

`permdock usage` cross-references three sets: the permissions the definition declares, the permissions the code checks, and the permissions the policy grants. Gaps between them are either dead code or security bugs, and both are cheap to catch in CI.

## Usage [#usage]

```bash
permdock usage                     # human-readable report
permdock usage --json              # machine-readable report with $schema
permdock usage --strict            # warnings become exit 1
permdock usage --ignore 'billing.plan.*'
```

Inputs come from `permdock.config.ts`: `permissions` (the definition module), `policy` (the server-only policy module) and `collect.srcPath` (where to look for usages). If `permissions.catalog.json` exists and is fresh, usages are read from it; otherwise the source scan from [collect](/docs/cli/collect) runs in memory.

## Findings [#findings]

### Defined but unused [#defined-but-unused]

A permission exists in `definePermissions()` but no source file under `srcPath` passes it to `can`, `decide`, `assert`, `filter`, `where`, `usePermission`, `getPermission`, `protect`, `registerTool`, a `tools` map or any other checker. Usually a leftover after a refactor, sometimes a permission that only exists to be granted to an agent's delegation. Reported as a warning.

### Used but ungranted [#used-but-ungranted]

A permission is checked somewhere, but no role in the policy has an `allow` for it. Every check will be `denied` for every subject, which is either intentional (a feature flagged off) or a missing grant that will surface as a support ticket. Reported as a warning; with `--strict` it fails the build.

### Granted by no role [#granted-by-no-role]

A permission is granted by an `allow` that belongs to a role fragment not passed to `definePolicy`, or the grant is unreachable because a `deny` in the same role always wins for the same permission with no condition. Reported as an error because it usually means a feature's `policy.ts` fragment was never merged. See [Larger apps](/docs/getting-started/larger-apps) for how fragments are composed.

### Conditions on undeclared fields [#conditions-on-undeclared-fields]

A grant's `where` or `check` reads a field that the resource's schema does not declare, for example `where: { ownerId: principal.id }` on a resource whose schema has `authorId`. The in-memory evaluator reads a missing field as absent, so the grant silently matches nothing, and the RLS compiler emits a column that does not exist. Only the first segment of a dotted path is checked. Resources whose schema exposes no JSON Schema through Standard JSON Schema (`~standard.jsonSchema`) are skipped. Reported as a warning.

### Client checks outside include [#client-checks-outside-include]

When every `snapshot(...)` and `snapshotFor(...)` call passes a literal `include` list, a check (`usePermission`, `can`, `decide`, `filter`, `actions`) on a permission outside all of those lists, in a client file, is reported. On the client that permission resolves as `server-only` or through the decision endpoint, which is usually a missing entry in `include` rather than a choice. A file is a client file when it carries `'use client'`, has `.client.` in its name, or matches `doctor.clientEntries` ([doctor](/docs/cli/doctor)). An unscoped snapshot, or an `include` that is not a literal list of permission references, turns the check off, because either one can cover every permission. Reported as a warning.

## Example report [#example-report]

```text
permdock usage

  defined but unused (2)
    billing.plan.change          features/billing/permissions.ts:14
    post.publish                 features/posts/permissions.ts:9

  used but ungranted (1)
    post.archive                 app/posts/[id]/actions.ts:31 (assert)

  granted by no role (1)
    billing.invoice.refund       features/billing/policy.ts:22 (role 'finance' not passed to definePolicy)

  conditions on undeclared fields (1)
    post.read                    role 'member' reads 'ownerId', which the post schema does not declare

  client checks outside include (1)
    post.delete                  app/posts/[id]/menu.tsx:12 (usePermission) is outside every snapshot include

  4 warnings, 1 error
```

## How grants are read [#how-grants-are-read]

`usage` imports the policy module and inspects `policy.roles` as data. Roles are arrays of grants, so no subject is needed and no condition is evaluated. Closures are counted as grants; their conditions are opaque to this command. The policy module runs in the CLI process only, never in a client build.

## Dynamic usages [#dynamic-usages]

Permissions resolved through `findPermission(permissions, someString)` cannot be attributed statically. They are listed under a `dynamic` section with their call sites so a reviewer can decide whether the string source (database, JWT scope, OpenAPI import) is trusted. `--dynamic-as-used` treats every permission as potentially used, which silences "defined but unused" for catalogs that are driven entirely by data.

## Why [#why]

* **Undeclared fields are a `usage` warning, not a `definePolicy` error.** A schema without Standard JSON Schema cannot be read, and some policies deliberately condition on a field added by a view or a join. A warning in CI catches the typo without making the policy module depend on schema introspection at runtime.
* **Include scoping is checked only when every snapshot is scoped with a literal list.** The check has to be certain what the client holds; with one unscoped or computed `include` it cannot be, and a guess would produce findings nobody can act on.

## CI [#ci]

```yaml
- run: pnpm exec permdock usage --strict
```

Recommended together with `collect --check`: `collect` guarantees the catalog reflects the code, `usage` guarantees the code and the policy agree.

## Related [#related]

* [collect](/docs/cli/collect)
* [doctor](/docs/cli/doctor)
* [Policies](/docs/concepts/policies)
* [Larger apps](/docs/getting-started/larger-apps)
