PermDock
CLI

usage

Report permissions that are defined but never used, used but never granted, or granted by no role.

Status: planned Phase: 2

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

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 runs in memory.

The three findings

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

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

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 for how fragments are composed.

Example report

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)

  2 warnings, 1 error

How grants are read

usage imports the policy module and inspects policy.roles as data (0010). 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

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.

CI

- 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.

Open questions

  • Whether usage should also flag grants whose portable condition references a field that the resource schema does not declare (currently a type error at policy definition time, which covers the common case).
  • Whether snapshot scoping (snapshot({ include })) should be checked for client routes that use permissions outside the included subtree.

On this page