PermDock
CLI

catalog

Export the permission catalog as JSON, JSON Schema or Markdown, and the shape of the catalog document.

permdock catalog turns the merged permission definition into documents that people, CI and agents read without running the app. collect scans source and writes permissions.catalog.json; catalog builds the same catalog, or reads the runtime definition with --permissions, and renders it in the requested format.

Usage

permdock catalog --out permissions.catalog.json
permdock catalog --kind json-schema --out permissions.schema.json   # JSON Schema of the catalog document
permdock catalog --kind markdown --out docs/permissions.md
permdock catalog --kind markdown --out docs/permissions.md --check   # CI: exit 1 when the file is stale
permdock catalog --permissions ./src/permissions.ts   # render straight from the definition module, skipping collect
permdock catalog --include post --include billing.invoice   # scope to a subtree
FlagMeaning
--kind <kind>catalog (default), json-schema or markdown
--permissions <module>The definition module to read; default permissions in permdock.config.ts
--include <key>Permission keys or prefixes to keep, repeatable
--out <file>Write the document to this file, with a Created or Updated line; without it, print it on stdout
--checkWith --out: exit 1 and print a diff when the file differs; write nothing

Where the data comes from

Permission leaves are plain JSON and the definition is a runtime value, so the catalog is produced by importing the definition module and walking listPermissions(permissions). Resource schemas are exported through Standard JSON Schema (~standard.jsonSchema.output(...)) when the validator supports it (Zod, Valibot and ArkType do); resources whose validator does not implement Standard JSON Schema get schema: null and a warning.

Usage sites and defining files come from the source scan; usages is always present and empty when no call site was found.

Catalog JSON shape

{
  "$schema": "https://permdock.com/schemas/catalog-v1.json",
  "version": 1,
  "generatedAt": "2026-09-06T10:00:00Z",
  "generator": "permdock@0.4.0",
  "resources": {
    "post": {
      "id": "id",
      "schema": {
        "type": "object",
        "properties": { "id": { "type": "string" } }
      },
      "definedIn": "features/posts/permissions.ts"
    }
  },
  "permissions": [
    {
      "key": "post.update",
      "scope": "post:update",
      "resource": "post",
      "action": "update",
      "arity": "instance",
      "meta": { "title": "Update post", "description": "Edit a post you own" },
      "usages": [
        { "file": "app/posts/[id]/page.tsx", "line": 42, "call": "assert" },
        { "file": "features/posts/policy.ts", "line": 12, "call": "allow" }
      ]
    },
    {
      "key": "post.create",
      "scope": "post:create",
      "resource": "post",
      "action": "create",
      "arity": "collection",
      "meta": {},
      "usages": []
    }
  ]
}

Notes on the shape:

  • permissions is a flat array sorted by key; nesting (billing.invoice.pay) is expressed in the dotted key, and resource holds the dotted resource path.
  • arity is instance or collection.
  • meta is whatever the definition attached to the action (title, description, tags, or any JSON); it is what MCP tool descriptions and Markdown output use.
  • schema is a JSON Schema document (draft 2020-12).
  • Without a configured policy, the catalog has no grants, role details or conditions. With one, grants lists every code grant with its portable conditions, approval, fields and validity, in a canonical order (wire formats); closures are marked portable: false and never serialised. diff compares that section between two versions.
  • The $schema URL is the published JSON Schema of the catalog format, which is the same document --kind json-schema emits, so agents can validate a catalog offline. The exact hostname is fixed when the docs site launches.

Reading a catalog

A package that reads the catalog at run time imports permdock/catalog, a runtime entry with no CLI code. A build script finds the file with catalogPath(config, cwd) from permdock/cli.

import { readFileSync } from "node:fs";
import { catalogPath } from "permdock/cli";
import { parseCatalog, rowConditionKeys } from "permdock/catalog";
import config from "./permdock.config.ts";

const catalog = parseCatalog(
  readFileSync(catalogPath(config, process.cwd()), "utf8"),
);
const conditioned = rowConditionKeys(catalog);
const sqlEnforceable = catalog.permissions.filter(
  (permission) => !conditioned.has(permission.key),
);
  • parseCatalog(json, { requireMeta? }) takes the JSON text or the parsed value. It checks it against schemas/catalog-v1.json, then runs the validation rules, and returns a deep-frozen copy built from own keys. A __proto__ key stays an ordinary key, and an object with any other prototype, a cycle or a value JSON cannot carry is rejected. On failure it throws PermDockValidationError with code: 'invalid-data', boundary: 'catalog' and every issue with its path.
  • rowConditionKeys(catalog) is the set of keys marked rowConditions: true: permissions whose code grants depend on more than role and scope, so a policy that calls only the SQL helpers would widen access (RLS).
  • operationFindings(previous, next) compares the operations of two catalogs by name and returns the operation-risk-raised and operation-approval-removed findings: a meta.risk that rose, or a permission that no longer lists approvals. A new or removed operation is not a finding.
  • catalogPath(config, cwd, out?) resolves the file the same way collect writes it: out, then collect.out, then catalog.out, then permissions.catalog.json.
  • The types (CatalogDocument, CatalogPermission, CatalogResource, CatalogRole, CatalogScope, CatalogGrant, CatalogValidity, CatalogApproval, CatalogBreakGlass, CatalogActivation, CatalogSupportAccess, CatalogUsage) are exported from both permdock/catalog and permdock/cli.

The reader, the $schema file and --kind json-schema come from one schema constant, and a test checks the committed catalogs against both the reader and Ajv.

Validation rules

After the schema passes, parseCatalog checks the references JSON Schema cannot express. It reports every broken rule at once, each issue with its path:

RuleIssue path
A role's on is a declared scope name, resource, or tenant or team when scopes is absentroles[i].on
An operation's permission and results are declared permission keysoperations[i].permission, .results
A grant's permission is a declared permission keygrants[i].permission
Every key a delegation or OAuth scope lists is a declared permission keydelegations[i].permissions[j], oauthScopes[i].permissions[j]
permissions[].key, roles[].key, scopes[].name, plans[].key and operations[].name are uniquethe second entry's key or name
With requireMeta, every permission carries those meta fields (title, risk, effects)permissions[i].meta.<field>

requireMeta is off by default: collect writes meta from what the code declares, and an application without a hosted console needs none of it. A consumer that displays or approves on these fields, such as PermDock Cloud, passes parseCatalog(json, { requireMeta: ['title', 'risk', 'effects'] }). permdock cloud push --dry-run prints the same issues before an upload.

Markdown output

--kind markdown renders one section per resource with a table of actions, arity, scope and metadata, and (when present) usage counts. It is intended to be committed under docs/ or pasted into a PR so reviewers can see the permission surface change without reading TypeScript.

Consumers

  • openapi reads the catalog to emit securitySchemes scopes and x-permdock-permissions.
  • The MCP adapter uses meta for tool descriptions and arity to decide whether a data loader is required.
  • The docs MCP server (search, list_pages and get_page at /docs/mcp) and llms.txt expose the Markdown output so agents can ask "which permissions exist?".
  • permdock/testing compares a policy matrix against the catalog to find permissions with no test.

Side effects

What each command may change; side effects explains each one.

CommandSide effects
permdock catalogfiles

Last updated on

On this page