CLI
The permdock CLI that ships in the permdock package, its commands, exit codes and how to run it in CI.
The permdock binary ships inside the permdock package, next to the runtime entries, and permdock/cli exports defineConfig and run. No runtime entry (permdock, permdock/next, permdock/jwt, ...) imports CLI code, so none of it reaches an application bundle. The CLI's dependencies are oxc-parser (to scan TypeScript source), citty (flags and help), jiti (to load TypeScript modules), smol-toml (supabase/config.toml), package-manager-detector (install hints), @clack/prompts (prompts on a terminal) and diff (the diff --check prints); pgsql-parser (rls import, rls migrate), pg (rls import --db, rls verify --db) and unplugin (permdock/unplugin) are optional peers the command loads only when it runs. When the peer is missing, the error is the install line for the package manager that ran the command, or the one whose lockfile the project has (pnpm add -D pg, npm i -D pg, yarn add -D pg, bun add -D pg). See adapters.
The CLI never evaluates a policy against a real subject. Only permdock cloud push needs a secret, which it reads from PERMDOCK_CLOUD_KEY. It reads permission definitions, source files, OpenAPI documents and database catalogs, and writes generated files, reports and diffs.
Install
pnpm add permdock
pnpm exec permdock doctor
# or run without installing
npx permdock doctorThe binary is permdock; permdock --version prints its version. All commands accept --cwd and --config permdock.config.ts. A config file is optional; every option can be passed as a flag.
// permdock.config.ts
import { defineConfig } from "permdock/cli";
export default defineConfig({
permissions: "./src/permissions.ts", // the merged definition module
policy: "./src/policy.ts", // server-only; used by usage, rls, doctor
collect: { srcPath: ["./src", "../ui/src", "./node_modules/@acme/*"] },
catalog: { out: "./permissions.catalog.json" },
rls: { tables: { post: "posts" }, dialect: "supabase" },
// or: rls: supabaseRls({ memberships: { table: 'organization_members', ... } })
});Commands
| Command | Purpose | Page |
|---|---|---|
permdock collect | Scan srcPath[] for definePermissions() and permissions.x.y usages; write the catalog and generated barrel; --check for CI | collect |
permdock catalog | Export the catalog as JSON, JSON Schema or Markdown | catalog |
permdock diff | Compare two policies or catalogs: changed permissions, scopes, roles and grants; --impact runs the fixtures through both; exit 1 on a breaking change | diff |
permdock usage | Report defined-but-unused, used-but-ungranted and granted-by-no-role permissions, conditions on undeclared fields, and client checks outside a snapshot include | usage |
permdock openapi | Emit security and securitySchemes into an OpenAPI document, or import one into a generated definition; --target 3.1|3.2|3.3 (3.3 experimental: pinned Security Profile draft), --format document|overlay, --overlay 1.1|1.2 (1.2 experimental: pinned reusable-actions draft), --profile fapi2, --profile-scheme, --arity, --authorization-url, --token-url, --check; import takes JSON or YAML with --schema, --map and --annotate | openapi |
permdock arazzo check | Resolve every Arazzo step to x-permdock-permissions; never evaluates a subject | arazzo |
permdock rls | generate, import and verify Postgres RLS policies; generate --split for Supabase declarative schemas (pg-delta's per-schema layout, or --grants-out for db diff); migrate moves hand-written policies onto the generated helpers and --retire-out drops a trigger-maintained permissions table; --helpers-only keeps policies hand-written, and --backfill-out copies existing custom roles | rls |
permdock doctor | Check wiring, imports, references, catalog freshness, skills, TypeScript version, sensitive verbs (PD017), static separation of duty (PD018), hosted-grant wiring (PD020, PD021), custom roles outside their ceiling (PD023), approvals the requester can give (PD024), token attributes a user could set (PD028) and declarative-schema hook grants and helper order (PD042, PD043) and server-only grants with no decision endpoint (PD044) | doctor |
permdock config | Warn on config keys PermDock does not read; --print the effective config with every default | config |
permdock skills | Install, update or check (--check) the PermDock Agent Skills (permdock, permdock-wire, permdock-audit and the topic skills) | skills |
permdock cloud push | Publish the catalog, hostable flags and role assignability to a PermDock Cloud environment; --dry-run, --url, --environment | cloud |
permdock supabase hook generate | Compile the app's fromTable / fromJunction membership sources into a Supabase Custom Access Token Hook migration with a size budget, authz_ver and IdP-row protection; --grants-out for declarative schemas, --check for CI | supabase |
permdock supabase inspect | Print the hook and helper manifest (helper names, tenant claim, budget, claims written, membership sources, deciding columns); --json for the version: 1 manifest, --out to write it (default permdock.manifest.json), --check for CI | supabase |
permdock powersync generate | Compile the read grants into PowerSync Sync Streams (sync-config.yaml); --check for CI | powersync |
permdock powersync verify | Fail when sync-config.yaml is stale or, with --db, when a stream syncs a fixture row the policy denies | powersync |
The Next.js build hook createPermDockPlugin is documented on Next.js plugin. The same collect hook for Vite, Rollup, webpack, Rspack and esbuild is createPermDockUnplugin (unplugin): Nuxt, Astro, React Router, TanStack Start and Effect apps use it; there is no per-vendor package.
Exit codes
All commands share one contract so CI steps can be written without parsing output.
| Code | Meaning |
|---|---|
0 | Success; for --check and verify, no drift and no findings |
1 | Findings: drift detected, parity failures, unused or ungranted permissions, doctor errors. Also a database or the Cloud that did not answer |
2 | Usage or configuration error: unknown flag, missing config, unreadable definition module, unsupported TypeScript version |
130 | Interrupted with Ctrl-C while a --db command was connected; the connection is closed first |
Warnings never change the exit code unless --strict is passed.
Output
Every command prints a human-readable report by default and supports --json for machine consumption. JSON reports carry a $schema field pointing at the report schema included in the package so agents can validate them. Under --json, every non-zero exit prints either the command's JSON report or RFC 9457 Problem Details on stdout, never plain text: type is https://permdock.com/problems/cli-usage (exit 2), https://permdock.com/problems/cli-unavailable (a database or service did not answer, exit 1) or https://permdock.com/problems/cli-failed (a check with a text report, such as collect --check drift, exit 1), with title, detail, command and exitCode. Paths in reports are relative to --cwd. --help (or permdock help) prints the command list; permdock <command> --help (or permdock help <command>) prints that command's flags, their values and defaults.
When --check finds drift (collect, openapi, rls generate, supabase hook generate), it prints each stale file's name followed by a unified diff from the file on disk to what the command would write, cut to the first 40 lines, so the CI log shows what changed without a local run.
Reports are styled on a colour terminal and plain in a pipe, unless FORCE_COLOR is set; NO_COLOR and --no-color turn styling off. A flag with a fixed set of values rejects any other with exit 2 and the list it accepts, for example catalog: Invalid value for argument: --format (xml). Expected one of: json, schema, markdown.
CI usage
# .github/workflows/permissions.yml
- run: pnpm exec permdock collect --check # catalog and barrel up to date
- run: pnpm exec permdock usage --strict # no ungranted or unused permissions
- run: pnpm exec permdock doctor # wiring, imports, TS version
- run: pnpm exec permdock openapi --check --doc openapi.json
- run: pnpm exec permdock arazzo check --doc workflows/publish-post.arazzo.json --openapi openapi.jsonpermdock rls verify --db $DATABASE_URL runs in the integration job that has a Postgres service; see rls. On Supabase, permdock rls verify --advisors runs the Supabase CLI's security advisors on the same database and exits 1 on a WARN or ERROR lint. Every --db connection gives up after 10 seconds and every statement after 60 seconds, and then exits 1.
How the CLI reads definitions
Permission definitions are runtime values, so most commands import the definition module and call listPermissions(); no type information is needed. Source scanning with oxc-parser is used only where usage sites matter (collect, usage, doctor) and never depends on the TypeScript compiler API, which TS 7.0 does not expose stably.
Config, permissions and policy modules are loaded with Node's import(). When Node cannot resolve or parse one, the CLI loads it again with jiti, which transpiles TypeScript and TSX on the fly. Imports then resolve the way the project's bundler resolves them: extensionless relative paths, enum and namespace, and the paths aliases of the nearest tsconfig.json above the module (following extends), so import { db } from '@/lib/db' works. permissions.ts may import schemas from Zod, Valibot or ArkType freely. The policy module is loaded only by usage, rls and doctor, and only in the process running the CLI.
permdock.config.ts configures the CLI only. permdock/testing fixtures are passed as arguments in the test files that use them.
Why
- The CLI takes dependencies; the runtime entries do not. An application bundle, an edge function or a Worker loads only the runtime entries, which depend on
@standard-schema/specand nothing else. The CLI runs on a developer machine or in CI, where a well-maintained package replaces code PermDock would otherwise maintain and get subtly wrong. The six CLI packages besidesoxc-parseradd about 2.7 MB unpacked, most of it the TypeScript transform inside jiti, and only@clack/promptsbrings dependencies of its own.tests/bundlefails if a runtime entry reaches any CLI package. - Heavy or rare dependencies stay optional peers.
pgsql-parser,pgandunpluginare large and serve one or two commands, so a project that never runsrls importnever installs them. - citty for flags and help. One definition per command gives the parser, the
--helptext, the value check for flags with a fixed set of values, and the list the docs test reads, so the help and the docs cannot disagree with the parser. - jiti for loading modules. Node's own type stripping rejects
enum, extensionless imports and tsconfigpaths, which most app code uses; jiti loads a module the way the app's bundler does. Node goes first because it is faster, and a module it loads is the same module the app's runtime loads. An error thrown while a module runs is reported as it is. - Prompts only at a terminal. A prompt in CI or a pipe would hang the job, so prompts appear only when stdin and stdout are a terminal,
CIis unset and--jsonis not set. Every prompt has a flag that answers it, and without a terminal the command behaves as the flag says.--yes(-y) turns prompts off at a terminal too.
Last updated on
Testing
permdock/testing ships policy matrix tests over roles, permissions and fixtures, snapshot fixtures for UI adapters, an RLS parity runner, and Vitest type tests.
collect
Scan source paths for definePermissions() calls and permission usages, write the catalog and generated barrel, and fail CI on drift.