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 doctorThe 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.
| Flag | Meaning |
|---|---|
--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 |
--json | Print 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, -y | Never 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 |
--debug | Log each step, and the cause of an error, on stderr |
--color, --no-color | Styled or plain output; the default follows the terminal, FORCE_COLOR and NO_COLOR |
--help, -h | Print the command's flags, examples, side effects and docs page |
--version, -v | Print 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
| Command | Purpose | Page |
|---|---|---|
permdock init | Write 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 commands | init |
permdock preset | decode a preset code into its archetype, features, stack and services, or print its /create url | preset |
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 gen | Write typed constants, row validators, JSON Schema and test factories from the policy; --check for CI | gen |
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 writes security and securitySchemes into an OpenAPI document or an Overlay; import turns a JSON or YAML document into a generated definition | 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 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 roles | rls |
permdock casl import | Turn CASL rules per role into a generated permissions and policy module; report what does not translate | casl |
permdock casbin import | Turn a Casbin RBAC model and policy CSV into a generated permissions and policy module, with user assignments as data | casbin |
permdock scan | List authorization checks that do not go through PermDock: role comparisons, hasRole helpers, CASL abilities, Clerk has() | scan |
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, the code policy and permdock.manifest.json to a PermDock Cloud project; --dry-run, --url, --project, --manifest | cloud |
permdock cloud pull | Write the published catalog and manifest of a Cloud project, byte for byte as collect and supabase inspect --out write them; --project | cloud |
permdock login / logout / whoami | Sign 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 use | login |
permdock supabase hook generate | Compile 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 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 in data, --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-url-stdin, when a stream syncs a fixture row the policy denies | powersync |
permdock scim generate | Write the tenant-keyed SCIM directory tables postgresDirectoryStore reads and writes, revoked from the data API's client roles; --dialect, --schema, --check for CI | scim |
permdock codemod 0.5 | Rename the identifiers and CLI flags 0.5.0 renamed in source files, scripts and workflows, and list the call sites whose shape changed; --dry-run | codemod |
permdock completion | Print the bash, zsh or fish completion script | completion |
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, and an unexpected error (internal) |
2 | Usage or configuration error: unknown flag, missing config, unreadable definition module, unsupported TypeScript version |
3 | Action required: the run needs input, a login or the person's consent; the JSON result lists the commands that continue it in next |
130 | Interrupted: Ctrl-C (SIGINT), or a cancelled prompt |
143 | Terminated (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:
| Effect | Meaning |
|---|---|
files | Writes generated files, all or nothing, with a Created or Updated line |
rewrite | Edits files the project owns, such as source files or migrations |
database:read | Connects to Postgres to read catalogs or run fixtures |
database:write | Writes to Postgres |
network | Calls a URL other than a database |
cloud:write | Publishes to a PermDock Cloud project |
install | Installs 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.jsonprintf %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/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. - 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, sopermdock --helpand--versionstart 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 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.
Agent mode
How the permdock CLI behaves when an AI agent runs it - JSON results by default, no prompts, exit 3 with next steps, and a consent token before it rewrites files, publishes to the Cloud or overwrites with --force.