Roles
A role maps a job to the slice of the shared content a person needs: which domains, which skills, rules, agents,
commands and checks, and how each skill is surfaced. generate --role <name> renders that slice, for the project or
with --user for one person’s machine. Use roles when one repository serves people with different jobs, such as
backend engineers, release managers and support agents.
Example
Section titled “Example”presets = ["claude", "cursor", "copilot"]
[role_manifest]enabled = true # write .ai-rulez/roles.json on generate
[[roles]]name = "engineer"description = "Everyone who writes code"domains = ["backend"]
[roles.skills]exclude = ["deploy-*"]
[roles.skill_mode]"review-*" = "name-only"
[roles.match]groups = ["okta:eng"]
[[roles]]name = "release-manager"extends = "engineer"domains = ["release"]
[roles.skill_mode]deploy = "user-invocable-only"The backend domain has the skills review-pr and deploy-staging; the release domain has deploy.
ai-rulez roles resolve release-managerrole release-manager: 4 item(s), 895 bytes, ~237 tokens (cl100k_base)KIND DOMAIN ID MODE DELIVERY BYTES TOKENSrule - commit-messages - - 153 42rule backend database - - 156 43skill backend review-pr name-only static 289 78skill release deploy user-invocable-only static 297 74
skill_mode not honored on every configured harness (see https://goldziher.github.io/ai-rulez/roles/#skill_mode-on-other-harnesses): backend/review-pr = name-only: not applied on copilot, cursor; the skill stays listed theredeploy-staging is excluded by the parent role. ai-rulez generate --role release-manager writes the two skills
and sets their modes for each tool. Claude Code gets them as skillOverrides in .claude/settings.json:
{ "skillOverrides": { "deploy": "user-invocable-only", "review-pr": "name-only" }}Cursor and Copilot read the shared .agents/skills/, where user-invocable-only becomes a frontmatter key:
---name: deploydescription: "Use when deploying to production."disable-model-invocation: true---name-only has no equivalent there, so review-pr stays fully listed and generate warns.
Profiles vs roles
Section titled “Profiles vs roles”Both select a slice of the same content, but they answer different questions.
| Profile | Role | |
|---|---|---|
| Question | what does this repository ship? | what does this person’s job need? |
| Scope | the project or team | a person (identity decided elsewhere) |
| Selected by | --profile, default |
--role |
| Extra knobs | none | per-kind selectors, skill_mode, delivery, match, pin |
| Compared by | tokens --compare-profiles |
tokens --by-role |
Use a profile when one checkout emits several project variants (a backend service, a frontend app) and
that choice belongs in the repository. Use a role when the same content is tailored to different people at
generation time. Domains are the shared vocabulary: profiles group domains for a project, roles group domains for a
person. Many projects keep a default profile for the repository and add roles for per-person variants.
Defining roles
Section titled “Defining roles”Roles are a [[roles]] array in config.toml. Manifest settings live in a separate [role_manifest] table, because
TOML cannot use roles as both an array and a table.
| Key | Type | Meaning |
|---|---|---|
name |
string | Lowercase letters, digits, - and _, starting with a letter or digit; unique. What --role takes. |
description |
string | Shown by roles list. |
domains |
list | Domains the role selects, as in a profile (builtin:<name> works too). Root content, globally enabled builtins and include-sourced domains are always kept. |
skills, rules, agents, commands, checks |
table | include and exclude lists of item ids or path.Match globs. An entry with / matches domain/id. An empty include keeps everything the domains provide; exclude always wins. Context has no selector: context files always stay. |
skill_mode |
table | Skill id or glob to on, name-only, user-invocable-only or off. |
delivery |
table | Skill id or glob to static, served or both; see Delivery. |
extends |
string | One parent role. |
match |
table | groups: hint strings for an external tool. Not inherited. |
pin |
bool | Record the digest of the role’s rendered outputs in ai-rulez.lock. Not inherited. |
[role_manifest] key |
Default | Meaning |
|---|---|---|
enabled |
false |
write .ai-rulez/roles.json on generate; see The roles manifest |
skill_mode_fallback |
drop |
what off does where a tool cannot hide a skill; see skill_mode on other harnesses |
Roles can be overlaid in config.local.toml like profiles: entries merge by name, and remove = true drops a
shared role on your machine.
skill_mode precedence
Section titled “skill_mode precedence”For one skill, every matching key competes: an exact id beats a glob, a longer glob beats a shorter one, and a tie
goes to the lexically first pattern. A skill no key matches keeps the default, on.
Claude Code’s skillOverrides is keyed by skill id, so two kept skills with the same id in different domains share
one entry. When the role gives them different modes, validate reports AR971; give both the same mode (a bare id
key does) or exclude one.
Inheritance
Section titled “Inheritance”extends is one level deep: a role whose parent extends another role is an error. The child is merged over the
parent:
| Field | Merge |
|---|---|
domains |
union, parent first |
exclude lists |
union |
include lists |
union when both set one, otherwise whichever is set |
skill_mode, delivery |
merged; the child wins per key |
description |
the child’s, else the parent’s |
match, pin |
not inherited |
Delivery
Section titled “Delivery”[roles.delivery] decides per skill whether the role’s agent gets it as a file in the tool’s skill folder (static),
only on demand from the skills server (served), or both. Keys follow the
skill_mode rules, and entries are inherited with the child winning.
[[roles]]name = "backend"domains = ["backend"]
[roles.delivery]"deploy-*" = "served" # not listed in the agent's context; found with find_skill"backend/runbooks" = "both"For one skill, the first that is set wins: its own delivery frontmatter, the role, [domains.<name>] delivery,
[skills] delivery, then static. Everything that renders or serves a role uses it:
generate --role backendleaves served skills out of the skill folders and adds thedynamic-skillsskill;tokens --role backenddoes not count served skills in the listing and names them;ai-rulez mcp --serve-skills --role backendserves them, withfind_skillscoped to the role;roles resolve,roles.jsonandcatalogreport each skill’s delivery;ai-rulez lockpins the served skills of every role, so[lock] enforcecovers them.
Commands
Section titled “Commands”ai-rulez roles list [--format json] # every role with item counts and token estimatesai-rulez roles show <name> [--format json] # as declared, and with the parent merged inai-rulez roles resolve <name> [--format json] # the items the role keeps, with sizes, modes and delivery
ai-rulez generate --role engineer # project outputs for the roleai-rulez generate --user --role engineer # user-level outputs (~/.claude, ~/.codex, ...)ai-rulez generate --check --role engineer # drift check against the role's outputs
ai-rulez tokens --role engineer # token surface of the roleai-rulez tokens --by-role # one comparison column per role--rolereplaces profile selection and cannot be combined with--profileor--plugin. Everything in the run (outputs, token report, usage index) sees the role’s slice.- Installed skills scoped to profiles are not filtered by a role, and the machine-local
local/tree is not narrowed. generate --roleprints the role’sAR971findings as warnings before it writes anything, so a typo in a domain or anexcludeentry is visible.
Composing roles
Section titled “Composing roles”--role takes a comma-separated list, as do roles show, roles resolve, tokens --role and
mcp --serve-skills --role. A composition is the union of its members: the domains and items they keep are combined,
and that union is rendered, served and counted.
ai-rulez generate --role engineer,oncallai-rulez roles resolve engineer,oncallWhere two members set skill_mode or delivery for the same skill, the precedence applies
first (the more specific key wins); between equally specific keys, the role listed last wins. So engineer,oncall
and oncall,engineer can differ.
A composition is a value, not a [[roles]] entry: it cannot be extended, need not be declared, and every member must
exist (AR971 otherwise). It can be pinned: ai-rulez lock --role engineer,oncall records the union’s outputs under
the name engineer,oncall, and generate --locked --role engineer,oncall compares them. Pinning a member does not
pin the compositions that contain it, and the reverse.
skill_mode becomes Claude Code skillOverrides
Section titled “skill_mode becomes Claude Code skillOverrides”| Mode | Claude Code behavior |
|---|---|
on |
listed with its description (the default) |
name-only |
listed by name only, saving description tokens |
user-invocable-only |
hidden from the model; you can still run it as /name |
off |
hidden from the model and from you |
For every kept skill with a mode, generate writes skillOverrides.<skill> in .claude/settings.json (or
~/.claude/settings.json with --user) with the same per-key ownership as
[claude.settings.managed]:
- Only the skill ids the role lists are owned; other entries are left alone, and
cleanremoves exactly what was written. A role’s modes win over the same skill in[claude.settings.managed] skill_overrides. - If you had set a different value for one of those ids by hand,
generate --rolewarns, replaces it, and keeps yours in<config dir>/local/.role-skill-overrides.json(machine-local, gitignored). A plaingenerate, or a role that no longer lists the skill, restores your value. - A value you change after the role wrote it is yours and is left alone.
- Switching roles removes the first role’s entries and writes the second’s.
mcp --serve-skills --rolenever writes the settings file.
skill_mode on other harnesses
Section titled “skill_mode on other harnesses”Only Claude Code has a per-skill settings key. For other tools, ai-rulez writes the mode into the skill’s SKILL.md
frontmatter (or Codex’s agents/openai.yaml) where the vendor documents an equivalent. A key you set in the skill’s
own frontmatter is never overwritten.
| Preset | user-invocable-only |
off |
name-only |
|---|---|---|---|
claude |
skillOverrides |
skillOverrides |
skillOverrides |
cursor |
disable-model-invocation: true |
no documented setting | no |
codex |
agents/openai.yaml policy.allow_implicit_invocation: false |
not rendered (Codex needs an absolute path) | no |
copilot |
disable-model-invocation: true |
disable-model-invocation: true and user-invocable: false |
no |
opencode |
no | not rendered (permission.skill in opencode.json) |
no |
gemini |
no | no settings key (only the /skills disable command) |
no |
| other presets | not documented | not documented | not documented |
Where a cell says “no” or “not documented”, the skill stays listed on that tool. roles resolve and doctor list
every skill and tool where a mode is not honored, and generate warns.
off hides a skill only when every configured tool can hide it. Otherwise generate --role follows
skill_mode_fallback:
[role_manifest]skill_mode_fallback = "drop" # drop (default) | servedrop leaves the skill out of the role’s render for every tool of the run, Claude Code included. serve switches it
to served delivery, so tools that can call MCP find it with find_skill.
Strict validation
Section titled “Strict validation”| Code | Meaning |
|---|---|
AR971 role-reference-unknown |
A domain that does not exist, or a selector, skill_mode or delivery entry that matches no item (or only items in a domain the role does not select). |
AR972 role-extends-invalid |
Unknown parent, cycle, or inheritance deeper than one level. Such a role is left out of roles.json. |
AR973 role-unreachable-dependency |
An item the role keeps names, in its skills: frontmatter, a skill the role drops or hides with off or user-invocable-only. |
A bad name, a duplicate name or an invalid skill_mode or delivery value fails loading outright.
The roles manifest
Section titled “The roles manifest”With [role_manifest] enabled = true, generate writes .ai-rulez/roles.json; roles list --format json prints the
same document. It has no timestamps and is sorted (roles by name, items by kind, domain and id), so it is safe to
commit. It is built from the shared sources only: roles from config.local.toml and items under local/ never
appear in it, so the file is the same on every machine. The schema is
schema/roles-manifest.schema.json.
{ "schema_version": 1, "tokenizer": "cl100k_base", "roles": [ { "name": "engineer", "description": "Everyone who writes code", "match": { "groups": ["okta:eng"] }, "domains": ["backend"], "skill_modes": { "review-pr": "name-only" }, "items": [ { "kind": "rule", "id": "commit-messages", "path": "rules/commit-messages.md", "bytes": 153, "tokens": 42 }, ... ], "totals": { "items": 3, "bytes": 598, "tokens": 163, "by_kind": { "rule": 2, "skill": 1 }, "served_skills": 0, "served_tokens": 0 } } ]}| Field | Meaning |
|---|---|
items[].bytes |
size of the item’s source files on disk (a skill counts its resources) |
items[].tokens |
estimate for the primary file (SKILL.md, the rule file), like every ai-rulez tokens figure |
items[].mode, items[].delivery |
the skill’s resolved mode and delivery |
items[].owner, items[].version |
from the item’s frontmatter, when set |
delivery |
the role’s [roles.delivery] table |
totals.served_skills, totals.served_tokens |
skills that are served only, which cost no listing tokens until loaded |
Integrating an identity tool or UI
Section titled “Integrating an identity tool or UI”Everything an integration needs is in versioned --format json output. ai-rulez never calls the network, and an
integration never parses config files.
- Discover the roles. Read the committed
roles.json, or runai-rulez roles list --format json. Each role has itsname,descriptionandmatch.groups. - Map a person to a role. This is your tool’s job: for example, read the person’s identity-provider groups and
pick the role whose
match.groupscontains one of them, by your own precedence when several match. - Preview.
ai-rulez roles resolve <name> --format jsonlists the items the role keeps, with owner, version, bytes and tokens.ai-rulez tokens --role <name> --format jsonreports the token surface. - Apply. Run
ai-rulez generate --user --role <name> --yes(user level) orai-rulez generate --role <name>(project level). Exit code 0 means the files were written. - Show the catalog.
ai-rulez catalog --format jsonlists every item with owner, version, size, digest, the roles that keep it and the lock status. See Catalog andschema/catalog.v1.schema.json. - Audit.
ai-rulez lock --diff --format jsonreports what changed between the committed lock and the working tree.
Every JSON document carries schema_version; refuse a version you do not know.
Roles and the lock file
Section titled “Roles and the lock file”The lock file pins each role declaration (kind = "role"), so adding a role or changing what it
includes shows up in lock --diff and fails lock --check. A role’s rendered outputs are pinned, as one digest per
role, when the role sets pin = true or with lock --roles. lock --check --role <name> and
generate --check --locked --role <name> then catch a change in what the role renders even when the sources are the
same (a new skill_mode entry, a new ai-rulez release); see Composing with roles.
Without a pin, generate --locked --role <name> still verifies that the sources match the lock. Served skills are
pinned as [[served]] entries; see Served skills and skill sources.
