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), jiti (to load TypeScript modules), smol-toml (supabase/config.toml), package-manager-detector (install hints), @clack/prompts (prompts on a terminal), diff (the diff --check prints) and fastest-levenshtein (did-you-mean for an unknown flag or value); pgsql-parser (rls import, rls migrate; version 18 or later, older majors are refused), pg (rls import --db-url-stdin, rls verify --db-url-stdin) 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 the Cloud commands need a credential: permdock cloud push reads PERMDOCK_CLOUD_KEY (or --key-stdin), cloud pull and rls generate --project read PERMDOCK_CLOUD_TOKEN (or --token-stdin), and all three fall back to the login permdock login stores. A database URL comes from --db-url-stdin or, when the command needs a database, DATABASE_URL then SUPABASE_DB_URL, the order better-supabase reads them; no flag takes a secret as its value, and a secret is never echoed in output, --debug logs or the next commands. 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. A config file is optional; every option can be passed as a flag. Commands are grouped: permdock rls generate, permdock openapi emit, permdock skills install. A group on its own prints its subcommands and exits 2.

Global flags

Every command takes these, before or after its own flags.

FlagMeaning
--cwd <dir>Run as if started in this directory; paths in reports are relative to it
--config <file>Config file; default permdock.config.{ts,mts,js,mjs} in --cwd. Only the commands that read the config load it
--jsonPrint the cli-result-v1 envelope on stdout; --no-json turns agent mode's default off
--format <format>text, json, github or sarif. github prints workflow annotations and sarif a SARIF 2.1.0 log, for commands that report findings
--yes, -yNever prompt; take the answer the flags give
--confirm <token>The token a consent_required result gave, once the person agreed (agent mode)
--agent <mode>auto (default), yes or no; also PERMDOCK_AGENT. See agent mode
--debugLog each step, and the cause of an error, on stderr
--color, --no-colorStyled or plain output; the default follows the terminal, FORCE_COLOR and NO_COLOR
--help, -hPrint the command's flags, examples, side effects and docs page
--version, -vPrint the version

An unknown flag or command exits 2 with the nearest match. A flag or command renamed in 0.5.0 exits 2 with its new spelling (flag_renamed, command_moved), and permdock codemod 0.5 rewrites scripts and workflows that use the old one.

// 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
  operations: "./src/operations.ts", // the defineOperations map; collect writes it to the catalog
  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 initWrite a preset's permissions, policy and framework wiring from a /create code or from flags; keep existing files unless --force; print the install and shadcn commandsinit
permdock presetdecode a preset code into its archetype, features, stack and services, or print its /create urlpreset
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 genWrite typed constants, row validators, JSON Schema and test factories from the policy; --check for CIgen
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 writes security and securitySchemes into an OpenAPI document or an Overlay; import turns a JSON or YAML document into a generated definitionopenapi
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 by default, --schemas migra with --grants-out for db diff); migrate moves hand-written policies onto the generated helpers and --retire-out drops a trigger-maintained permissions table; explain shows the tables, membership sources, custom roles and triggers the SQL binds to; --helpers-only keeps policies hand-written, and --backfill-out copies existing custom rolesrls
permdock casl importTurn CASL rules per role into a generated permissions and policy module; report what does not translatecasl
permdock casbin importTurn a Casbin RBAC model and policy CSV into a generated permissions and policy module, with user assignments as datacasbin
permdock scanList authorization checks that do not go through PermDock: role comparisons, hasRole helpers, CASL abilities, Clerk has()scan
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, the code policy and permdock.manifest.json to a PermDock Cloud project; --dry-run, --url, --project, --manifestcloud
permdock cloud pullWrite the published catalog and manifest of a Cloud project, byte for byte as collect and supabase inspect --out write them; --projectcloud
permdock login / logout / whoamiSign in to the platform issuer (device flow, or --loopback with PKCE), store the token outside the repository; logout revokes it; whoami names the credential in uselogin
permdock supabase hook generateCompile the app's tableMembershipSource / junctionMembershipSource membership sources into a Supabase Custom Access Token Hook migration with a size budget, authz_ver and IdP-row protection; --schemas for the schema workflow, --grants-out for db diff, --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 in data, --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-url-stdin, when a stream syncs a fixture row the policy deniespowersync
permdock scim generateWrite the tenant-keyed SCIM directory tables postgresDirectoryStore reads and writes, revoked from the data API's client roles; --dialect, --schema, --check for CIscim
permdock codemod 0.5Rename the identifiers and CLI flags 0.5.0 renamed in source files, scripts and workflows, and list the call sites whose shape changed; --dry-runcodemod
permdock completionPrint the bash, zsh or fish completion scriptcompletion

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, and an unexpected error (internal)
2Usage or configuration error: unknown flag, missing config, unreadable definition module, unsupported TypeScript version
3Action required: the run needs input, a login or the person's consent; the JSON result lists the commands that continue it in next
130Interrupted: Ctrl-C (SIGINT), or a cancelled prompt
143Terminated (SIGTERM)

Warnings never change the exit code unless --fail-on warning is passed. Errors lists every problem code with its exit code.

Output

stdout carries the command's data and nothing else; progress, warnings and logs go to stderr. Every command prints a text report by default. --json (or --format json, or agent mode) prints one cli-result-v1 envelope instead, success or not:

{
  "$schema": "https://permdock.com/schemas/cli-result-v1.json",
  "status": "success",
  "data": { "...": "the report the command's page describes" }
}

status is success, error or action_required. A failure carries error, RFC 9457 Problem Details with type, title, detail, command, exitCode and a stable snake_case code such as unknown_flag, config_invalid, dependency_missing, dirty_worktree, network_error or check_failed; type is the code's section on the errors page, such as https://permdock.com/docs/cli/errors#unknown_flag. The codes match the better-supabase CLI's wherever both report the same condition. next lists the commands that continue the run. The JSON Schema ships as schemas/cli-result-v1.json in the package, next to catalog-v1.json and supabase-manifest-v1.json.

--format github prints the findings of doctor, usage, config, scan and arazzo check as GitHub Actions annotations, and --format sarif as a SARIF 2.1.0 log for code scanning. Paths in reports are relative to --cwd. collect --watch prints one JSON line per rebuild under --json.

--help (or permdock help) prints the command list; permdock <command> --help (or permdock help <command>) prints that command's flags, examples, side effects and docs page. --help --json prints the same in the cli-commands-v1 format (schemas/cli-commands-v1.json): name, version, the global options, and commands, each with name, path, description, docs, examples, effects, config, arguments and options. <command> --help --json prints the same document with that command only. An agent reads it to learn the CLI without scraping text; the shape follows better-supabase's.

When --check finds drift (collect, catalog, openapi emit, rls generate, supabase hook generate, supabase inspect, powersync generate, scim 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, per stream, 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: --kind 'xml' is not one of catalog, json-schema, markdown.

Side effects

Each command declares what it may change, and --help prints it on the Side effects line:

EffectMeaning
filesWrites generated files, all or nothing, with a Created or Updated line
rewriteEdits files the project owns, such as source files or migrations
database:readConnects to Postgres to read catalogs or run fixtures
database:writeWrites to Postgres
networkCalls a URL other than a database
cloud:writePublishes to a PermDock Cloud project
installInstalls packages

A command names its main output --out, as better-supabase does. --out - prints it on stdout for catalog, rls generate, supabase inspect and scim generate, with notes and warnings on stderr; a command that only writes a file (collect, rls import, supabase hook generate, powersync, openapi emit and openapi import, the importers) refuses - with invalid_value. A second file it can split off is a named output with its own flag (--grants-out, --seeds-out, --backfill-out, --retire-out), which writes nothing unless set.

A command that writes files takes --dry-run to print the plan, or --check to compare with the files on disk, and writes nothing in either case. It never overwrites a file PermDock did not write unless --force is set. A rewrite command refuses a file with uncommitted changes in a git work tree (dirty_worktree, exit 3) unless --force is set, so the edit can be reviewed and undone with git. In agent mode, rewrite, cloud:write and --force need the person's consent.

CI usage

# .github/workflows/permissions.yml
- run: pnpm exec permdock collect --check # catalog and barrel up to date
- run: pnpm exec permdock usage --fail-on warning # no ungranted or unused permissions
- run: pnpm exec permdock doctor --format github # wiring, imports, TS version, as annotations
- run: pnpm exec permdock openapi emit --check --doc openapi.json
- run: pnpm exec permdock arazzo check --doc workflows/publish-post.arazzo.json --openapi openapi.json

printf %s "$DATABASE_URL" | permdock rls verify --db-url-stdin 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-url-stdin 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.
  • One manifest for the parser, the help, completion and the docs test. A static table names every command, its group, summary, examples, side effects and docs page, and each command module declares its flags once. The parser, --help, --help --json, completion and the test that reads these pages all come from it, so they cannot disagree. The table loads no command module, so permdock --help and --version start without importing one; each command loads when it runs. A small parser of PermDock's own replaced citty, which had no subcommands with their own flags, no strict unknown-flag check and no machine-readable help.
  • 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