PermDock

For AI agents

How a coding agent should install, wire, check and audit PermDock, and how PermDock's errors and denials are written for models.

PermDock's second audience is the coding agent working in a TypeScript repository. Every page, skill, error message and CLI report is written so that an agent can wire PermDock end-to-end, verify its work and explain a denial without reading library source. This page is the entry point for that agent. The permdock skill and the permdock-* skills ship in the permdock package. Point an MCP client at https://permdock.com/mcp for search and page-fetch; agent docs standards has install links for Cursor and VS Code and the Claude Code command.

Start here

npx skills add ScaleDockHQ/PermDock

This installs eight Agent Skills into the folders your agent reads (.agents/skills/, .claude/skills/, .cursor/skills/); --skill <name> installs one:

  • permdock: the mental model and invariants, reading a Decision, explain for an unexpected denial, the docs MCP, and which skill to load next. Start here.
  • permdock-wire: detect the framework and validator, create src/permissions.ts, src/policy.ts and the factory file, add the first check and the first UI guard, run the CLI checks, add CI steps.
  • permdock-audit: review an existing installation or a pull request for ungranted or unused permissions, validation and identity gaps, untested grants, and the OWASP Agentic Top 10 mapping; it runs each topic skill's Verify list.
  • permdock-agents, permdock-approvals, permdock-tenancy, permdock-data, permdock-credentials: one area each, from agent delegation and human approval to named scopes, queries and RLS, and tokens, API keys and share links.

The same skills ship inside the permdock npm package and can be installed with permdock skills install, so the skill version always matches the library version. In Claude Code, /plugin marketplace add ScaleDockHQ/permdock adds them as the permdock plugin from .claude-plugin/marketplace.json. See skills.

What the repository gives you

ArtifactWhereUse it for
AGENTS.md (CLAUDE.md imports it)repository rootMaintainer guide: layout, commands, the invariants and an index of the topic rules
.agents/rules/*.mdcrepository root, linked into .cursor/rules and .claude/rulesMaintainer rules attached by path: invariants, naming, docs, "when you change X also update Y", testing, prose
llms.txt, llms-full.txtdocs site root, and under /docsIndex and full text of the docs for context loading
.md per pageappend .md to any docs URLRead one page as Markdown without HTML
Docs MCP serverPOST /mcp (search, list_pages, get_page)Search and read docs from inside an MCP-capable agent
Docs WebMCP toolsevery docs page (search_docs, read_page, open_page)Search and read docs from the browser's own agent; experimental, Chrome 149+ behind a flag
Cloud MCP serverhttps://mcp.permdock.comRead-only evidence, catalog and pending approvals; never resolves an approval
permissions.catalog.json and its JSON Schemaapp repository, permdock catalog --format schemaKnow which permissions exist, their arity, scopes and metadata
permdock doctor --jsonapp repositoryMachine-readable findings with stable codes and fixes
Example appsapps/examples/<adapter>Working code for every adapter, each checked by permdock doctor

Standards for these artifacts are on the Agent docs standards page.

The shortest wiring recipe

Three files and one check. Import paths and identifiers follow the naming convention; do not invent framework-prefixed names.

// src/permissions.ts — importable everywhere; no rules, no secrets
import { definePermissions, defineRoles, resource, crud } from "permdock";
import { Post } from "./schemas"; // any Standard Schema validator

export const permissions = definePermissions({
  post: resource(
    Post,
    crud({
      id: "id",
      actions: { publish: { title: "Publish post" } },
    }),
  ),
});

export const roles = defineRoles({ member: {} });
// src/policy.ts — server-only
import { definePolicy, role, allow, principal } from "permdock";
import { permissions, roles } from "./permissions";

const member = role(roles.member, [
  allow(permissions.post.read),
  allow(permissions.post.list),
  allow(permissions.post.create),
  allow(permissions.post.update, { where: { authorId: principal.id } }),
  allow(permissions.post.delete, {
    where: { authorId: principal.id },
    approval: "human",
  }),
  allow(permissions.post.publish, { approval: "human" }),
]);

export const policy = definePolicy(
  { permissions, roles },
  {
    roles: [member],
    principal: (user: User | null) =>
      user && { id: user.id, roles: user.roles },
  },
);
// src/permdock/server.ts — the explicit factory; same pattern for permdock/hono, permdock/mcp, permdock/ai-sdk
import { createPermDock } from "permdock/next";
import { policy } from "../policy";

export const { getPermDock, getPermission, PermDockProvider, permdockHandler } =
  createPermDock(policy, {
    subject: async () => getUser(await cookies()),
  });

Then, in a Server Component or Server Action:

const permdock = await getPermDock();
permdock.assert(permissions.post.update, post);

And in a client component:

import { usePermission } from "permdock/react";
const { allowed, status } = usePermission(permissions.post.update, post);

Finish with pnpm exec permdock doctor and add permdock collect --check and permdock usage --strict to CI. Full versions: quick start, Next.js adapter, MCP adapter, AI SDK adapter.

The shortest OpenAPI recipe

If the project already generates an OpenAPI description, do not write security by hand and do not add a PermDock wrapper around the generator. Emit PermDock's Overlay and let the existing pipeline apply it:

permdock openapi emit --doc public/openapi.json --format overlay --out permdock.overlay.json

For Next.js and the other frameworks next-openapi-gen scans, add overlay: { apply: ['./permdock.overlay.json'] } to openapi-gen.config.ts; for Redocly pipelines, redocly join openapi.json --overlay permdock.overlay.json -o dist/openapi.json. Hey API, Orval, Scalar and any OpenAPI-to-MCP bridge then read the applied description with no PermDock code. Rules: every operation needs an operationId; do not also declare security through the producer (@auth, authPresets) on operations PermDock covers; add permdock openapi emit --format overlay --check to CI. Details: OpenAPI adapter, adapters.

Invariants an agent must keep

Use permission references, never strings; keep the policy and every createPermDock call out of client files (permdock doctor reports it as PD001); take the subject only from the verified session or token, never from a model argument; prefer portable where conditions to closures; and put approval: 'human' on destructive actions an agent can reach. Everything else fails closed: a Decision is only ever granted, denied or approval-required. The full list, with the reason for each rule, is the threat model.

How errors and denials are written for models

PermDock assumes the reader of a denial may be a language model deciding what to do next, so denials carry structure rather than prose:

  • decide() returns { outcome: 'denied', denials: [{ role, reason }], alternatives: [...] }. reason is the policy author's string; alternatives lists permissions on the same resource this subject does hold, so the model can pick a permitted action instead of retrying the same one.
  • approval-required carries the grant, a reason and a token. Adapters turn it into AI SDK user-approval, WorkflowAgent needsApproval, MCP input_required or an HTTP 403 with the .../approval-required problem type; the token must be echoed back when the approval resumes.
  • MCP refusals return isError: true with the Decision in structuredContent, and missing delegation produces a scopeChallenge naming the exact scope (post:delete) to request.
  • HTTP adapters emit RFC 9457 application/problem+json with type, title, permission, denials and alternatives; see Errors and Problem Details.
  • PermDockValidationError names the resource and the schema issues; PermDockDeniedError and PermDockApprovalRequiredError carry the Decision.
  • CLI reports use stable codes (PD001...) with a one-line fix each, and --json output carries a $schema.
  • simulate([[permission, data], ...]) lets an agent pre-flight a whole plan and read every decision before acting; it is the in-process form of AuthZEN evaluations. simulate({ arazzo, openapi }) does the same from an Arazzo workflow; a provisional: true step is not a final grant.

Reading the docs

  • Concepts first: Permissions, Policies, Decisions, Subject, Authentication.
  • One adapter page per framework, each with the same sections (purpose, API, lifecycle, validation, denials, example app, standards): Adapters.
  • Design rationale lives in the Why sections of the owning pages; cite them instead of relitigating a constraint: permissions, Next.js plugin, decisions.
  • A Status: planned or Status: tracking line under a page's frontmatter means the thing does not exist yet; a page without one describes code in the package. Undecided design questions are listed on the roadmap.

Security framing for agent code

PermDock's agent features map onto the OWASP Top 10 for Agentic Applications: per-tool permissions with approval: 'human' gates and boundary validation address ASI02 Tool Misuse; the two-principal subject and delegation intersection address ASI03 Identity and Privilege Abuse. When writing tool handlers, wire the permission on the tool (permission in registerTool or the tools map) rather than checking inside the handler, so list_tools and capability middleware can hide what the caller may not do.

Last updated on

On this page