PermDock
CLI

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 doctor

The 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

CommandPurposePage
permdock collectScan srcPath[] for definePermissions() and permissions.x.y usages; write the catalog and generated barrel; --check for CIcollect
permdock catalogExport the catalog as JSON, JSON Schema or Markdowncatalog
permdock diffCompare two policies or catalogs: changed permissions, scopes, roles and grants; --impact runs the fixtures through both; exit 1 on a breaking changediff
permdock usageReport defined-but-unused, used-but-ungranted and granted-by-no-role permissions, conditions on undeclared fields, and client checks outside a snapshot includeusage
permdock openapiEmit 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 --annotateopenapi
permdock arazzo checkResolve every Arazzo step to x-permdock-permissions; never evaluates a subjectarazzo
permdock rlsgenerate, 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 rolesrls
permdock doctorCheck 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 configWarn on config keys PermDock does not read; --print the effective config with every defaultconfig
permdock skillsInstall, update or check (--check) the PermDock Agent Skills (permdock, permdock-wire, permdock-audit and the topic skills)skills
permdock cloud pushPublish the catalog, hostable flags and role assignability to a PermDock Cloud environment; --dry-run, --url, --environmentcloud
permdock supabase hook generateCompile 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 CIsupabase
permdock supabase inspectPrint 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 CIsupabase
permdock powersync generateCompile the read grants into PowerSync Sync Streams (sync-config.yaml); --check for CIpowersync
permdock powersync verifyFail when sync-config.yaml is stale or, with --db, when a stream syncs a fixture row the policy deniespowersync

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.

CodeMeaning
0Success; for --check and verify, no drift and no findings
1Findings: drift detected, parity failures, unused or ungranted permissions, doctor errors. Also a database or the Cloud that did not answer
2Usage or configuration error: unknown flag, missing config, unreadable definition module, unsupported TypeScript version
130Interrupted 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.json

permdock 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/spec and 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 besides oxc-parser add about 2.7 MB unpacked, most of it the TypeScript transform inside jiti, and only @clack/prompts brings dependencies of its own. tests/bundle fails if a runtime entry reaches any CLI package.
  • Heavy or rare dependencies stay optional peers. pgsql-parser, pg and unplugin are large and serve one or two commands, so a project that never runs rls import never installs them.
  • citty for flags and help. One definition per command gives the parser, the --help text, 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 tsconfig paths, 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, CI is unset and --json is 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

On this page