# catalog

Source: https://permdock.com/docs/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. It is the read-only sibling of [collect](/docs/cli/collect): `collect` scans source and writes `permissions.catalog.json`; `catalog` renders that catalog (or the runtime definition directly) in the requested format.

## Usage [#usage]

```bash
permdock catalog --format json     > permissions.catalog.json
permdock catalog --format schema   > permissions.schema.json     # JSON Schema of the catalog document
permdock catalog --format markdown > docs/permissions.md
permdock catalog --from ./src/permissions.ts   # render straight from the definition module, skipping collect
permdock catalog --include post --include billing.invoice        # scope to a subtree
```

## Where the data comes from [#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 [#catalog-json-shape]

```json
{
  "$schema": "https://permdock.com/schemas/catalog-v1.json",
  "version": 1,
  "generatedAt": "2026-09-06T10:00:00Z",
  "generator": "permdock@0.1.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; the `target` (`draft-2020-12`, `openapi-3.0`) is selectable with `--schema-target`.
* 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](/docs/concepts/wire-formats#catalog)); closures are marked `portable: false` and never serialised. [diff](/docs/cli/diff) compares that section between two versions.
* The `$schema` URL is the published JSON Schema of the catalog format, which is the same document `--format schema` emits, so agents can validate a catalog offline. The exact hostname is fixed when the docs site launches.

## Reading a catalog [#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`.

```ts
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)` takes the JSON text or the parsed value. It checks it against `schemas/catalog-v1.json` 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](/docs/adapters/rls#sql-helper-contract)).
* `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 `--format schema` come from one schema constant, and a test checks the committed catalogs against both the reader and Ajv.

## Markdown output [#markdown-output]

`--format 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 [#consumers]

* [openapi](/docs/cli/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 `/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.

## Related [#related]

* [collect](/docs/cli/collect)
* [diff](/docs/cli/diff)
* [Wire formats](/docs/concepts/wire-formats)
* [Standard Schema](/docs/standards/standard-schema)
* [For AI agents](/docs/for-ai-agents)
