Skip to content
Docs menu

Policy reference

This page is the lookup reference. For a guided introduction with examples, see Policy authoring.

Policy source resolution

When you run any Kratex command, the CLI resolves a policy through three slots in order (first match wins):

SlotSourceHow
1Walk-up file discoveryLooks for kratex.policy.json starting at CWD and climbing to the filesystem root. Stops at the first match.
2Rules cacherules-cache/rules.json under the per-user Kratex config directory. If present, it is validated as a full policy and used ahead of the bundled default, surfaced as policy source remote; an unparseable or invalid cache file is ignored with a warning.
3Bundled default policyBundled in the CLI binary. Uses mode: "enforce". Always matches.

Mode source order

For CLI commands, the effective mode is resolved in this order (first match wins):

  1. --audit or --enforce flag on the command.
  2. mode field in the policy file.
  3. Resolver default: "enforce". A policy that omits mode enforces; Kratex never defaults to observe-only.

The KRATEX_MODE environment variable is read only when the runtime is loaded directly as a preload hook (node --import with KRATEX_REGISTER=1), where it overrides the policy file’s mode. It has no effect on CLI commands: kratex run, kratex install, and kratex ci set it on the child process themselves.

Top-level schema

{
  version: 1                     // required, always 1
  mode?: "audit" | "enforce"
  root?: string                  // base for relative paths; defaults to file directory
  metadata?: PolicyMetadata
  groups?: PolicyGroups
  rules?: PolicyRule[]
  sensitivity?: SensitivityPolicy
  dataClasses?: Record<string, { displayName: string; category?: string; description?: string }>
                                 // declare your own data-class ids for sensitivity and conditions
  telemetry?: { sinks: SinkConfig[] }
  extensions?: Record<string, unknown>   // vendor metadata; keys must be namespaced (contain a ".")
}

Unknown top-level keys are rejected at parse time.

Subject schema

{
  kind?: "package"               // default
  source?: "first-party" | "third-party" | "unknown" | "any"
  package?: string               // exact package name
  packageGroup?: string          // name from groups.packages
}

Every field is optional. With none set, or subject omitted entirely, the rule matches any caller at the lowest specificity: an absent source filters nothing and an absent package pins nothing. Narrow by source (origin) or by package / packageGroup (specific packages); a package match outranks a source match.

A second subject form matches editor extensions (the Guardian lane) by extension id:

{
  kind: "extension"              // required to select this form
  source?: "first-party" | "third-party" | "unknown" | "any"
  extension?: string             // exact extension id, publisher.name
  extensionGroup?: string        // name from groups.extensions
}

Target schema

Every target requires kind; all other fields are optional filters, combined with logical AND. A target with only kind matches every operation of that kind: { "kind": "network" } matches any outbound connection, { "kind": "fs" } any read or write. Each field you add narrows the match further.

fs

{
  kind: "fs"
  operation?: "read" | "write"
  path?: string       // glob; ~ expands to home directory
  pathGroup?: string  // name from groups.paths
}

network

{
  kind: "network"
  operation?: "connect" | "dns" | "unix-socket" | "custom-lookup" | "host-header-override"
  host?: string       // glob
  hostGroup?: string  // name from groups.hosts
  port?: number       // 1-65535
  protocol?: string
  path?: string       // URL path glob
  pathGroup?: string
}

process

{
  kind: "process"
  operation?: "spawn" | "shell"
  command?: string        // exact binary name or absolute path
  commandGroup?: string   // name from groups.commands
  args?: string[]
  argsMatch?: "exact" | "prefix" | "any"   // default: "exact"
  cwd?: string
  env?: string[]
}

env

{
  kind: "env"
  operation?: "read" | "write" | "delete" | "enumerate"
  name?: string       // exact variable name
  nameGroup?: string  // name from groups.env
}

runtime

{
  kind: "runtime"
  capability?: "workers" | "cluster" | "native-addons" | "native-binding"
              | "ffi" | "wasi" | "inspector" | "vm" | "dynamic-code"
              | "patched-api-replacement"
}

All capabilities are enforced on every supported Node version. One authoring detail: rules targeting the ffi capability by name fire on Node 22.15+; on older versions those loads are caught under native-addons. See Node.js compatibility.

install

{
  kind: "install"
  operation?: "lifecycle-script"
  package?: string
  packageGroup?: string
  script?: string   // e.g. "postinstall"
}

Condition schema (when)

All fields are optional. All specified fields must match (logical AND).

{
  phase?: "runtime" | "install" | "build" | "test" | "dev" | "ci"
  platform?: string | string[]          // "darwin", "linux", "win32"
  arch?: string | string[]              // "x64", "arm64"
  nodeVersion?: string                  // semver range
  packageVersion?: string               // semver range
  dependencyKind?: "dependencies" | "devDependencies"
                  | "optionalDependencies" | "peerDependencies"
  ci?: boolean
  cwd?: string
  env?: Record<string, string | boolean>
  extensions?: Record<string, unknown>  // vendor metadata; keys must be namespaced (contain a ".")
  exposed?: string | string[]           // "*" or specific label
  targetClasses?: string | string[]
  targetOutsideSubjectRoot?: boolean
  targetOutsideProjectRoot?: boolean
  targetExistsBeforeWrite?: boolean     // fs write ops only
}

Effect schema

{
  action: "allow" | "audit" | "block"   // required
  severity?: "info" | "low" | "medium" | "high" | "critical"
  reason?: string
  tags?: string[]          // cannot include "kratex.overrides-user" or "kratex.enforce-in-audit" (reserved)
}

Groups schema

groups: {
  packages?:   Record<string, { include: string[], exclude?: string[], description?: string }>
  extensions?: Record<string, { include: string[], exclude?: string[], description?: string }>
  paths?:    Record<string, { include: string[], exclude?: string[], description?: string }>
  hosts?:    Record<string, { include: string[], exclude?: string[], description?: string }>
  env?:      Record<string, { include: string[], exclude?: string[], description?: string }>
  commands?: Record<string, { include: string[], exclude?: string[], description?: string }>
}

Each group has at least one include entry. exclude is optional. extensions holds extension ids (publisher.name) and backs subject.extensionGroup; extension ids and package names are separate namespaces with separate group maps.

Sensitivity schema

Declares the path, environment-variable, and host patterns that carry data-class labels. Each entry is either a bare pattern string or a { pattern, classes } object.

sensitivity: {
  paths?: (string | { pattern: string; classes?: string[] })[]
  env?:   (string | { pattern: string; classes?: string[] })[]
  hosts?: (string | { pattern: string; classes?: string[] })[]
}

A bare pattern carries the generic sensitive class; the object form assigns the listed classes. Built-in classification already labels common credential paths (credentials), crypto wallets (wallet), and similar before any sensitivity block is added. Classes feed the exposed and targetClasses conditions. For a guided walkthrough, see Sensitivity and data classes.

Rule precedence

When multiple rules match an operation, the engine picks one winner using these dimensions in order (the first non-zero delta decides):

  1. Immutable tag. A rule carrying kratex.overrides-user outranks any rule without it. No user rule can carry this tag.
  2. Subject specificity. Named package (highest) > named group > source class > unspecified (lowest).
  3. Target specificity. Literal paths/hosts score higher than globs; globs score higher than omitted fields. More constrained targets win.
  4. Condition specificity. More when keys means higher score.
  5. Origin. User rules beat built-in rules at equal specificity.
  6. Package-chain block beats allow. When two package-constrained rules both match but their matched caller frames are different packages with different actions, block wins.
  7. Index tiebreaker. Later position in the rules array wins.

When no rule matches, the operation is allowed by default with no severity and no tags.

Common parse errors

MessageCause
Kratex policy <path> must be an objectTop-level value is not an object: e.g. a bare array or scalar.
rules[N].id duplicate id "<id>"Two rules in the policy share the same id.
duplicate rule id: <id>A rule id collides with a built-in rule id. Checked after schema validation, when built-in and user rules are merged.
<path> <message>Zod schema validation failure. <path> is a dotted path with bracketed array indices (rules[0].subject.package).
ENOENT: no such file or directory, open '<path>'A discovered policy file exists but cannot be read (e.g. a broken symlink). A permissions failure surfaces as EACCES instead.
Failed to parse Kratex policy <path>: …Invalid JSON: most commonly a comment or trailing comma. kratex.policy.json is strict JSON, so neither is allowed; the underlying parser message follows the colon.