Migrating to v5¶
This page collects the breaking changes of the v5 release and what ai-rulez migrate v5 does about each. The
detail of every change is in the changelog.
v5 also changes what ai-rulez is: a standards-compliant lifecycle tool for agent knowledge and capabilities (author, generate, bundle, validate, govern, publish). Nothing in your 4.x configuration needs to change for that; the positioning decides which formats the tool generates and checks. Standards carries the pinned spec versions and test status. In short:
| Standard | Status | Generate / bundle | Lint / validate |
|---|---|---|---|
| OKF (Open Knowledge Format, v0.2) | supported | export okf, the okf preset |
okf validate |
| Agent Plugins | supported | generate --plugin, publish --emit agent-plugins |
validate --strict |
| ARD (Agentic Resource Discovery) | supported | publish --emit ard |
validate |
| Agent Skills | supported | generate |
validate (partial) |
| AGENTS.md | supported | generate |
validate |
| llms.txt | supported | the llms-txt preset |
validate |
| MCP server config | supported | generate |
validate (partial) |
| MCP server card | planned | not generated | none |
| CycloneDX / SPDX SBOM | supported | sbom |
sbom --check |
| in-toto / DSSE / Sigstore | supported | sign |
verify --attestation |
| OpenTelemetry (OTLP) | supported | telemetry export --to otlp |
not applicable |
| "Agent bundle" | planned | spec to be confirmed | spec to be confirmed |
Upgrade in four steps¶
- Commit or stash your work, then preview:
ai-rulez migrate v5 --dry-runprints the change list and writes nothing. - Run
ai-rulez migrate v5. It rewrites the project in place (add--recursivefor a monorepo with several.ai-rulez/directories).--checkexits 2 while anything still needs migrating, for CI. - Run
ai-rulez doctor. It reports removed presets with a replacement, drifted outputs and generated paths git does not ignore. - Run
ai-rulez generate, review the diff and commit the sources and outputs together.
To migrate a child plugin from its repository root, select its config directory or file:
ai-rulez migrate v5 -C plugin/.ai-rulez --dry-run
ai-rulez migrate v5 --config plugin/.ai-rulez/config.toml
-C / --config selects the project to migrate, including configs under .config/ai-rulez/.
With --recursive, migration starts at the selected project root.
migrate only reads a 4.x project. A 2.x or 3.x project must first be migrated to 4.0 with ai-rulez 4.x
(npx ai-rulez@4 migrate v4), then with ai-rulez migrate v5. Every v5 command that meets an older version stops
with one actionable error: 2.x and 3.x say "install ai-rulez 4.x to migrate it to 4.0, then run ai-rulez migrate
v5", 4.x says "run ai-rulez migrate v5".
What migrate v5 does¶
| Rule | Rewrite |
|---|---|
version |
version = "4.x" becomes version = "5.0", keeping the rest of the line (including a trailing comment). |
convert-format |
A 4.x config.yaml, config.yml or config.json is converted to config.toml with keys, order and comments kept, and the old file is removed. The $schema key becomes schema. |
lint-ratchet |
[lint.budget] and [lint.tolerate] are renamed [lint.ratchet] (per-rule finding counts). [lint.budgets.<kind>] (size limits) is unchanged. |
preset-rename |
The windsurf preset becomes devin (outputs move from .windsurf/ to .devin/, delete the old directory) and the removed continue-dev preset is dropped. With --write, windsurf_model in agent frontmatter becomes devin_model. |
mcp-merge |
A legacy mcp.toml, mcp.yaml or mcp.json (4.x read the first of them) is folded into [[mcp_servers]] of config.toml and removed. When config.toml already defines mcp_servers, nothing is merged: the file is left in place and the report warns, so you can move the servers over by hand. |
pin-default |
The three defaults v5 changes are pinned to their 4.x value so the generated output does not move: agents_md = false, gitignore = true and [header] hashes = "full". Each pin carries a comment; delete the line to take the v5 default. A key you already set is never touched. |
local-overlay |
config.local.yaml, .yml and .json become config.local.toml (mode 0600). |
command-rename |
ai-rulez usage ... (hook, record, feedback, export, prune) and ai-rulez report usage|evals inside hook, verifier and script commands become ai-rulez telemetry .... |
frontmatter-alias |
The pre-4.24 frontmatter spellings permission_mode and user_invocable (Claude Code ignores them) become permissionMode and user-invocable. Without --write they are only reported; with it the markdown sources are rewritten. |
Options:
--dry-run: compute and print the change list, write nothing.--check: like--dry-run, and exit 2 when a project still needs migration (0 when none does).--adopt-defaults: do not pin the 4.x defaults; take the v5 ones (agents_md = true, no managed.gitignoreblock, content-only headers). Regenerate and review the diff.--write: also rewrite frontmatter aliases in the.ai-rulezmarkdown sources.--recursive: migrate every project found below the current directory.--format json: a machine-readable report (schema_version, one entry per project withstatus,changes,warningsanderror).
A migrated project is a fixed point: running migrate v5 again changes nothing. The rewrite is text-level, so
comments survive, and the result is decoded again before it is written; a file that would not load is reported and
left alone. Exit codes: 0 migrated or nothing to do, 1 a project could not be migrated, 2 --check found work.
YAML and JSON configs are not loaded in v5 (only config.toml is), so migrate converts a config.yaml,
config.yml or config.json to config.toml; see below.
Breaking changes¶
Each entry says what changed, what migrate v5 does, and what remains for you.
Config format and files¶
version = "5.0"is the only accepted config version. A4.xconfig is rejected withrun ai-rulez migrate v5;2.x/3.xwith the instruction to install ai-rulez 4.x first. Migrate: rewrites the version.- YAML and JSON configs are no longer loaded (
config.yaml,config.yml,config.json, theconfig.local.*forms and the YAML or JSON form ofmcp.*). Any command that finds one stops withYAML and JSON configs are no longer read ...: run ai-rulez migrate v5. Migrate: converts them to TOML. The YAML frontmatter of markdown content is unaffected. - Separate
mcp.toml,mcp.yaml,mcp.jsonfiles are no longer read. Migrate: merges them intoconfig.toml.ai-rules-mcp.schema.jsonis removed. init --format yaml|jsonis removed;initwritesconfig.toml.migrate v4is removed.migratetakes one target,v5.[lint.budget]is now[lint.ratchet](andratchet_exceededin the JSON report,Over ratchetin Markdown). It never meant a size limit;[lint.budgets.<kind>]is. Migrate: renames the table.
Before:
After ai-rulez migrate v5:
version = "5.0"
# Pinned by `ai-rulez migrate v5`: the 4.x default. Remove this line to take the v5 default.
agents_md = false
# Pinned by `ai-rulez migrate v5`: the 4.x default. Remove this line to take the v5 default.
gitignore = true
[lint.ratchet]
AR401 = 3
# Pinned by `ai-rulez migrate v5`: the 4.x default. Remove this line to take the v5 default.
[header]
hashes = "full"
Defaults¶
agents_md = trueby default.AGENTS.mdis the single canonical instruction file (root and nested scopes);CLAUDE.mdis a shim that imports@AGENTS.md; skills go to.agents/skillswhere the harness reads them. Setagents_md = falsefor the old per-harness files. Migrate: pinsagents_md = falseunless you pass--adopt-defaults.- Generated headers carry only the per-file
Content-Hash. The project-wideSource-Hashline, which rewrote every generated file on any edit, is gone by default.[header] hashes = "full"brings it back and"none"drops both. Migrate: pinshashes = "full". - The managed
.gitignoreblock is opt-in.gitignoredefaults to off, because committed outputs should not flip-flop in and out of the ignore block. Machine-local outputs (config.local.*,local/content) are still excluded automatically.generate --gitignoreorgitignore = trueturns the block on. Migrate: pinsgitignore = true.
Commands and flags¶
validateruns the content checks by default. Whatvalidate --strictdid is now plainvalidate(globs that match nothing, dead links, missing hooks, oversize content, the security rules).--config-onlykeeps the old config-only behavior.--strictnow means warnings fail, the same as--fail-on warning, as it already did fordoctorandverifiers run; it cannot be combined with--config-onlyor another--fail-on. A CI job that ranai-rulez validateand passed can now exit 2 on findings: fix them, lower a rule with[lint.severity], record them with--update-baseline, or pin the old check withvalidate --config-only. Replacevalidate --strictbyvalidatein scripts (keep--strictonly when warnings should fail).usage ...andreport usage|evalsare folded intotelemetry ..., with no aliases:
| 4.x | 5.0 |
|---|---|
ai-rulez usage hook |
ai-rulez telemetry hook |
ai-rulez usage record |
ai-rulez telemetry record |
ai-rulez usage feedback |
ai-rulez telemetry feedback |
ai-rulez report usage <log> |
ai-rulez telemetry report [log] |
ai-rulez report evals |
ai-rulez telemetry report evals |
telemetry record handles skill loads and item loads in one command, so one hook block records both. Hook
blocks already written into .claude/settings.json by hand run the old command and must be regenerated with
ai-rulez telemetry hook. Migrate: rewrites the commands inside config.toml hooks and verifiers.
- One flag vocabulary. --json / -j is gone everywhere: use --format json (--format text is the
default). generate --no-fetch / -f is now --offline, as on mcp. The confirmation skip of clean and of
remove / domain remove / profile remove / include remove / skill remove is now --yes / -y
(it was --force). convert --force, eval run --force and import okf --force (overwrite) are unchanged.
- Flag taxonomy sweep. There are seven shorthands and each means one thing everywhere: -C --config,
-D --debug, -T --token, -q --quiet, -y --yes, -n --dry-run and -o --output. Every other
shorthand is removed, and a removed spelling is an unknown flag whose error names its replacement. Old to new:
| 4.x | 5.0 |
|---|---|
-n <dir> (--config-dir, on most commands) |
--config-dir <dir>, global, no shorthand; -n is now --dry-run |
-d (--dry-run on generate and clean) |
-n / --dry-run (every command with --dry-run has -n) |
-p (--profile) |
--profile |
-p (--priority on add and edit) |
--priority |
-p (--path on include add and skill install) |
--path |
-d (--domain on add, edit, show, remove, list) |
--domain |
-d (--domains on init) |
--domains |
-s (--description, --source, --skip-content, --set-default) |
the long flag |
-c (--content), -t (--targets, --install-to), -m (--merge-strategy), -b (--budget) |
the long flag |
-r (--recursive, --ref), -i (--gitignore, --include), -w (--watch) |
the long flag |
-e / -E (--env, --env-file), -F (--from), -H (--setup-hooks) |
the long flag |
export okf --out dir / -o dir |
export okf --output-dir dir |
eval run --out dir, eval import --out dir, publish emit --out dir |
--output-dir dir |
review --out f, review fix --out f, review calibrate --out f, search --out f, verifiers run --out f |
--output f / -o f |
generate --strict (unknown configuration keys fail) |
generate --strict-config (env AI_RULEZ_STRICT=1 unchanged) |
lock --strict (a refused served skill fails) |
lock --refuse-findings |
sign --policy <file> (sign an organization policy) |
sign --org-policy <file>; --policy is the policy to evaluate |
ai-rulez validate path/.ai-rulez and every other [config-file] argument |
ai-rulez -C path/.ai-rulez validate |
--policy* and --discover-org accepted by every command |
only on the commands that evaluate policy |
--strict is now the shortcut of --fail-on warning and exists on validate, doctor and verifiers run only;
--check always means compare, write nothing, exit 2 on a difference. The positional config path is gone from
generate, validate, scan, clean, doctor, tokens, cost, sign, verify, export okf, llm doctor,
scanners list and verifiers run|list|explain: -C <path> takes a config directory or file and --config-dir
names the directory below the working directory, both global, so a command given a stray argument fails with the
-C spelling in the hint. The --policy, --policy-* and --discover-org flags are on generate, validate,
scan, lock, doctor, verify, catalog, mcp, sbom, approve, tokens, cost, publish and update;
elsewhere use AI_RULEZ_POLICY and the managed policy path, which every command honors. See
Flag conventions.
- Removed deprecated flags: generate --update-gitignore (use --gitignore), --no-configure-cli-mcp / -M
and --skip-cli-mcp / -S (they had no effect).
- The content commands report what they changed. add, remove, domain add|remove and edit print the created,
removed or rewritten path alone on a line of stdout (the "added successfully" sentence is on stderr and -q
hides it), and add, remove, edit, domain, profile, include and skill install|remove take
--format json for a {"status", "type", "name", "path", ...} document (schema schema/change-result.schema.json).
Migrate: scripts that scraped the INFO ... successfully lines read the path from stdout or use --format json.
- show and edit complete the verbs (list, show, add, edit, remove): ai-rulez show rule style,
ai-rulez edit rule style --content .... They are the CLI side of the MCP read_* and update_* tools.
- Group commands print help. ai-rulez list with no subcommand used to exit 1 with "specify what to list"; like
add, remove, domain, profile, include, skill, builtins and migrate it now prints its help and exits 0.
- Stricter content names and values (also for the MCP tools): a name must not end in .md, contain whitespace or
start with . or -; --targets must be preset names, paths or globs (--targets claude,bogus is an error);
minimal is a valid --priority, as the flag help always said. remove checks the item exists before it asks
to confirm. domain remove refuses while a profile lists the domain (it used to leave a dangling profile).
add skill without --description writes a placeholder that passes validate (it was the bare name, which
failed AR802). Migrate: rename files that violate the rules; validate flags them.
- migrate has real subcommands. ai-rulez migrate v5 and ai-rulez migrate okf are unchanged as command
lines, but are now subcommands: migrate alone prints help, migrate v4 or migrate banana is an unknown
command, and migrate v5 / migrate okf list in shell completion. --dry-run, --check and --format work
before or after the target; --adopt-defaults, --write and --recursive belong to v5 and okf rejects them.
The spellings migrate 5, migrate V5 and migrate v5.0 are gone.
- Removed aliases and dead flags. The aliases g (of generate), clear (of clean), v and check (of
validate) and check (of list checks) are removed; use the full names (gen and val stay). The hidden,
never-implemented mcp --transport, --address and --port are removed (the server is stdio only).
- guard is listed in --help. It stays a hook the harness runs.
- init prints the created directory on stdout (and takes --format json); its file listing and next steps are
on stderr, and the replace prompt is on stderr too. init --config-dir is the global flag.
- MCP init_project honors with_agents (it creates agents/code-reviewer.md; the flag was accepted and ignored).
- Report commands share one flag vocabulary and one JSON contract. Migrate: rename the flags below; read the JSON contracts for the documents.
| 4.x / earlier 5.0 build | 5.0 |
|---|---|
sbom --format cyclonedx\|spdx-json |
sbom --type cyclonedx\|spdx-json (--format is text\|json, the report of a gate, --check or -o) |
telemetry hook --format json\|toml |
telemetry hook --syntax json\|toml |
eval run printed Markdown by default |
eval run prints text by default; pass --format markdown to keep the old output |
lock --strict |
lock --refuse-findings |
generate --strict |
generate --strict-config (--strict now only ever means "warnings fail": validate, doctor, verifiers run) |
verify (generated files against their Content-Hash) |
generate --check. verify now checks only signatures, approvals and plugin provenance (--attestation, --approvals, --self, --plugin); a bare verify exits 1 and names generate --check |
publish verify --format json printed the bare result of a single dist directory |
always {"schema_version": 1, "results": [...]} |
--format json is also new on publish emit, verifiers explain and verifiers test, and verify --plugin.
roles list --format json prints "roles": [] instead of null when no role is defined, and scanners doctor
without a configured scanner says so instead of printing nothing. A failure under --format json prints the
error document for every command; the schema of every report is published in schema/.
- Exit codes follow one contract: 0 ok, 1 the command could not run (or the configuration is invalid or an older
version), 2 findings or drift. See the CLI reference. lock keeps its documented codes.
Output streams, errors and environment¶
- Results go to stdout, diagnostics to stderr, and
-qnever hides a result.list rules|context|skills|agents|commands|checks,domain list,profile list,include list,skill list,clean --dry-runandlock --checkused to print through the logger on stderr, so-qerased them. They now print on stdout. Migrate: scripts that read stderr for these lists read stdout, or use--format json.-qnow removes progress, information and success lines only; warnings, errors and hints stay (it used to hide warnings too). - One error rendering for every command.
Error: <message>, an optionalValidation errors:list andHint: <hint>on stderr. TheERROR Failed to <verb> ... error=... hint=...log-style errors of theadd,remove,list,domain,profileandincludefamily, the doubledERRORplusError:lines ofvalidateandscan, and the bare messages ofreview,eval runandtelemetry enableare gone. Under--format jsona failure also writes{"status": "error", "error": ..., "hint": ..., "exit_code": n}to stdout (it carriesschema_versionlike every document). Migrate: scripts that grep stderr for the old wording matchError:; scripts that parse stdout under--format jsonnow always get a document. - Exit codes are unchanged (
0ok,1could not run, usage errors included,2findings or drift,3lockonly), but are now produced in one place. A refused confirmation (clean,remove,--yesmissing in a non-interactive shell) exits1, as before;generate --user --cleandeclining a prompt used to exit0and now exits1. - Confirmation prompts are written to stderr, not stdout, so a piped stdout is never polluted.
- Unprefixed environment variables are ignored.
DEBUG,QUIETandVERBOSEused to change the CLI's output (they were read by an unprefixedAutomaticEnv). UseAI_RULEZ_DEBUG=1/AI_RULEZ_QUIET=1.AI_RULEZ_*variables are listed in the CLI reference. ~/.ai-rulez.{toml,yaml,json}and./.ai-rulez.*are no longer read by the root command. They were never used for settings; the lookup only printed "Using config file".--verbose/-Vis removed (it did nothing beyond that line). Use--debug/-D.--config-diris a global flag. Commands that declare their own--config-dir/-nkeep it.initandmigrateno longer declare one: they read the global flag. The content commands (add,remove,show,edit,list,domain,profile,include,skill) honor the global--config-dir <name>and-C <dir>(they used to ignore both and only auto-detect.ai-rulez, then.config/ai-rulez). Migrate: a script that passed--config-dirto one of them and relied on it being ignored now acts on that directory.--format text|jsonon more commands:generate(--check,--dry-run, the summary),clean,sign,export okfandversion.
Outputs¶
[[plugins]]no longer writes.claude/plugins.jsonor.codex/plugins.json. No tool read them. The table is still accepted andgeneratewarns that it has no effect. Use[claude.settings] manage = truewithenable_pluginsfor Claude Code and[plugins."name@marketplace"] enabled = truein.codex/config.tomlfor Codex. Migrate: warns; delete the files it left behind.- Every
--format jsondocument carriesschema_version. List commands (list,domain list,profile list,include list,skill list,builtins list) and multi-profiletokensnow return{"schema_version": 1, "items": [...]}instead of a bare array. Schemas:schema/validate-report,cost-report,telemetry-doctor,eval-report,okf-validate,catalog,roles-manifest,lock-diffandconvert-report(*.schema.json). A change to a document's shape bumps itsschema_version. ai-rulez.lock,[lock] enforce,http://sources,scan_importsand the trust model: see the sections below.
Other changes¶
The changes below came with the same release; none is touched by migrate v5.
| Change | Action |
|---|---|
Go module is github.com/Goldziher/ai-rulez/v5 and the entry point is cmd/ai-rulez |
go install github.com/Goldziher/ai-rulez/v5/cmd/ai-rulez@latest; update Go imports |
windsurf is renamed devin; continue-dev is removed |
Rename or remove the preset, delete old outputs |
[lock] enforce is on whenever ai-rulez.lock exists |
Commit a current lock, or set enforce = false |
The lock tree digest, pinned sources and frontmatter hook scripts changed |
Run ai-rulez lock once |
Exit codes follow one contract, lock adds 3 |
Update scripts that match exit codes |
lock --check without a lock file exits 1 |
Run ai-rulez lock first, or expect 1 |
http:// and git:// remotes are rejected |
Switch to https:// or ssh:// |
| The git token goes only to allowlisted hosts | Set AI_RULEZ_GIT_TOKEN_HOSTS for non-GitHub hosts |
| A committed config cannot reach outside the project | Move such paths to config.local.toml or the user config |
| Symlinked content must resolve inside the project | Replace links that leave the project |
scan_imports is on by default |
Fix findings, or set scan_imports = "off" |
scan runs only the security analyzer |
Use validate for the other findings |
Claude MCP servers are written only to .mcp.json |
None; .claude/settings.json loses mcpServers |
[telemetry] service_name is user scope only |
Move it to the user config or the environment |
| Eval results are signed per user | Use eval run --force in CI |
| Usage log is version 3 | None; older logs still read |
schema/catalog.schema.json is catalog version 2 |
Point validators of version 1 at schema/catalog.v1.schema.json |
generate warns about unknown config keys and new commands |
Fix the keys; set --yes or AI_RULEZ_ACK_COMMANDS=1 in CI |
Staged scanners are confined under isolation = "auto" (the default) wherever a backend works |
A scanner that writes outside its scratch directory now fails (AR9E3): point it at TMPDIR/HOME, or set isolation = "none"; see Isolation |
| Custom preset and provider output paths are validated | Remove .., absolute and .git paths |
lock and update scan every remote tree they pin to something new |
Fix error findings, or pass --accept-findings after reviewing them |
generate refuses an existing file it did not write (a hand-written CLAUDE.md) and never writes through a symlinked output |
Run ai-rulez convert --write to import the file (the first generate then replaces it), move it, or pass generate --force; see Existing files |
init --from runs through convert --write |
Expect one context item per root file (convert --split-headings splits it); see init --from |
generate --check reports blocked: for a shared file a machine-local input would change |
Commit or drop the local change, or run generate --allow-local-drift |
| The forge client (release dates, review-linked approvals) has its own host allowlist | GitHub Enterprise: set AI_RULEZ_FORGE_HOSTS |
| An organization policy at the managed path is read automatically | None unless the machine has one; see Organization policy |
Review-linked approvals count only reviews of the final head by members or named approvers; max_age is a ceiling |
Re-run approve --from-github-review after new pushes; see Approvals |
Go APIs under internal/ changed; pkg/airulez is the supported API |
See Go API |
The compression option is gone (it was a no-op since v3.13) |
Delete it; a config that still sets it loads and generate warns about the unknown key, but validate and generate --strict-config fail |
OKF is the format of .ai-rulez/¶
.ai-rulez/ is becoming an OKF bundle: each concept carries type, title and x-ai-rulez frontmatter and
every directory has an index.md. The current layout keeps loading during the deprecation window, so nothing breaks
and nothing needs to change on upgrade. To convert a project:
ai-rulez migrate okf --dry-run # list what would change
ai-rulez migrate okf # convert in place; run it again and nothing changes
ai-rulez generate --check # the generated files are byte-identical
migrate okf moves the frontmatter of each rule, context file, skill, agent, command and check under
x-ai-rulez.metadata, adds type (Decision for rules, Concept for context, Playbook for skills, Reference
otherwise; an existing type or title is kept) and writes the index.md files. Bodies are not touched, and neither
are skill and command resources (references/, scripts/, assets/). A file with an unclosed frontmatter block is
skipped and reported. --check exits 2 while a tree still needs migration.
Things to know:
type,titleandx-ai-rulezare reserved frontmatter keys. A native file that usedtypeortitleas its own key no longer sees it as metadata.index.mdandlog.mdthat only list entries (headings, bullet links) are listings, not content. A rule that happens to be calledindexwith prose in it is still a rule.validaterunsokf validateon a tree that has a rootindex.mdand reports theAR9B*findings, failing at--fail-on(defaulterror).- Not yet:
add,initand the MCP CRUD tools still write the native layout, and a customtitleis not restored byexport okffrom a migrated tree.
init --from¶
init --from now runs convert --write with its sources: importer names (auto, native, rulesync, ...) or the
project paths it always took (.claude, .cursor, CLAUDE.md). It gets convert's scan, validation and lossiness
report. Differences from v4:
- A root file such as
CLAUDE.mdbecomes one context item. Useai-rulez convert --split-headingsto split it. - MCP files, hooks and permissions are imported too (hooks and
allowrules as a commented block you review first). - The sources are checked in a scratch directory first. An existing configuration directory is moved aside until the
import has been written and is restored when the import fails, so a failed
init --fromleaves the old configuration in place. - Symlinks in the imported repository are never followed, and files over 2 MiB are skipped.
convert --fetch, new in v5, reads remote rulesync and APM sources over https:// only; ssh, scp-style, file:// and
local sources are reported as needs-action instead of being cloned.
Presets¶
| Change | Action |
|---|---|
windsurf is renamed devin. The output directory .windsurf/ is now .devin/ and the agent frontmatter key windsurf_model is now devin_model. There is no alias. |
Rename the preset in config.toml, rename windsurf_model keys in agent files, and delete the old .windsurf/ outputs. |
continue-dev is removed. It has no replacement. |
Remove it from presets. doctor reports it as an error. |
antigravity no longer adds the ai-rulez MCP server on its own. |
The [mcp] self_server default is now true, so generate adds the ai-rulez server to .agents/mcp_config.json and the root .mcp.json unless you set [mcp] self_server = false (or pass --no-self-mcp). |
amp no longer writes .agents/agents, which Amp does not read. |
None. Agents are listed in AGENTS.md. amp_model has no effect. |
codex and antigravity write commands as skills (.agents/skills/<id>/SKILL.md) instead of .codex/prompts and workflows. |
None. Files from the old layout are removed on generate. |
codex writes skills to .agents/skills, not .codex/skills. |
Set codex_skills_dir = ".codex/skills" to keep the old location. |
cursor user-level skills go to ~/.agents/skills, shared with codex, gemini and pi. |
Run generate --user; clean --user removes the old copies recorded in the manifest. |
opencode writes MCP servers as mcp.<name> with enabled, not mcp.servers.<name> with disabled. |
None. Members recorded under mcp.servers are removed. |
zoocode inlines rules into AGENTS.md and no longer writes .roo/rules. |
None. |
Generation behavior¶
- Divergent shared outputs fail. When two presets render different content to the same path,
generatefails and names them instead of keeping the last one. Make the outputs agree, for example withagents_md = trueorrules.mode = "inline". See Supported harnesses.qodernext to a tool that reads${VAR}references in the shared.mcp.json(claude,cursor,copilot,codebuddy,commandcode,reasonix) also fails, naming both, instead of writing a resolved secret. - Claude MCP servers move out of
.claude/settings.json. Claude Code reads project MCP servers from.mcp.json, which references${VAR};.claude/settings.jsonno longer receivesmcpServers, so resolved env values do not land in a committed file. An entry an earlier version wrote there is removed on the nextgenerateorcleanwhile it is still the value ai-rulez wrote. The settings file is written only when[claude.settings]manage,[[hooks]],[permissions]or[claude.settings.managed]apply. generatewarns about unknown config keys inconfig.tomlandconfig.local.toml, naming the nearest known key.generate --strict-config(orAI_RULEZ_STRICT=1) fails instead.validatefails on them as before.generatesummarises new or changed commands: hook commands, command-based MCP servers,[permissions] allowrules,[claude.settings.managed] env, plugin enablement andhttp/prompthooks that are new since the previous run on this machine (all of them in a fresh clone), printed even with--quiet. It only warns. Pass--yesor setAI_RULEZ_ACK_COMMANDS=1in CI to silence it.cleankeeps hand-edited generated files with a warning;clean --forceremoves them. The committed.ai-rulez/.generated-manifest.jsonlists merged-document paths only; claims and digests live in the gitignored.generated-manifest.local.json. A forged committed manifest cannot makegenerateorcleandelete or strip anything: a file is removed only when its ownContent-Hash(or the local digest) proves ai-rulez wrote it.- Check outputs are committed, not gitignored. Hosted reviewers read checks from the base branch, so the
files under Checks are never added to the managed
.gitignoreblock. An entry an earlier version added is removed on the nextgenerate. generate --dry-runprintsunchanged:andedited:for files that need no write. Scripts that matchwrite-file:for every file need updating.tokenstotals include the item listing.alwaysandheadline_alwaysnow count the skill, command and agent listing, so--budgetcan fail where it passed. The previous figures arealways_legacy,conditional_legacyandheadline_always_legacy.- Skill
evals/directories are not bundled into plugins unless[plugin] include_evals = true. - Custom preset and provider paths are validated. A custom preset
path, a provider spec path (root.file,outputs.*.dir, sidecars),okf.dirandmarketplace.output_dirare rejected when they contain.., are absolute or drive-qualified, or name.git,.ai-rulez,.hgor.svn.generatefails closed on any write outside the project or inside.git. A custom preset that writes a CI or tool-executed file (.github/workflows/,Makefile, ...) appears asexec-filein the command summary.
Lock file and enforcement¶
ai-rulez.lockpins content, under one hashing scheme.locknow also pins the ai-rulez version, every authored rule, context file, skill, agent, command, hook, role and settings source, the generated outputs and served skills, assha256:digests. Remote includes, OKF includes, installed skills and skill sources use the same scheme (there is no second, older per-file hash). Scripts (.sh,.py,.js, ...) are hashed byte for byte: a changed line ending in a script is a changed digest.lock --checkexits 2 on a lock without content pins, whatever[lock] enforcesays. A lock with anotherversionis refused with the instruction to runai-rulez lockagain. A lock that pins role outputs ([roles]pin = true) is written asversion = 2, which an older v5 build refuses instead of misreading the role pins; a lock without role pins staysversion = 1. See Lock file.- Run
ai-rulez lockonce after upgrading, review the diff and commit the file. The lock reads as stale until you do, for these reasons: - The
treedigest now also covers thesource,refandpathof remote entries and theviewof served entries, so relabeling or swapping entries is detected. A lock written by an earlier v5 build reports the tree digest as stale. lockpins the project scripts run by agent, skill and command frontmatterhooks. Editing such a script makeslock --checkexit 2; locks of items with frontmatter hook scripts needlockonce.- Local-path includes are pinned as
local-includeitems, and include skills are recorded asinclude:<name>/<path>instead of a machine-specific cache path. - Include and skill sources are recorded exactly as written in the config, and a
file://source stays machine-independent. Locks written earlier keep working until the nextlock. - Symlinks in a pinned tree are pinned by their link target. A symlink inside an include, installed skill, skill source or OKF tree is never followed and no longer blocks locking: the link target string is part of the digest, so a retargeted link is detected. Junctions and other irregular entries are pinned by path only. A symlinked root directory is still refused. Trees without symlinks keep their digest. File modes digest by the owner execute bit only, as git records it.
[lock] enforcedefaults totruewheneverai-rulez.lockexists. Setenforce = falseto opt out. A remote include or installed skill the lock does not cover makesgeneratefail (as--lockedalways did) andAR010an error, and an include that cannot be resolved is an error instead of a skipped warning.generate --frozenand--lockedare unchanged: they require the lock whether or not enforcement is on.[[skills]]is not a config key.skillsis the dynamic-loading table, so a[[skills]]array of tables stops the load with an error naming the line. Keep skills in.ai-rulez/skills/<name>/SKILL.md, install them withai-rulez skill install([[installed_skills]]), or point at a repository with[[skill_sources]].- An include that cannot be resolved is an error, with or without a lock. Before, an unreachable remote include
(no network, a deleted repository, no cached copy) was a warning and
validate,doctor,generateandgenerate --checkexited0while rendering without it. They now exit1and name the include.--no-fetchkeeps the old behaviour (a warning, the include skipped). [lock] enforce = trueis strict. It makesvalidate --strictreportAR981(source drift) andAR982(output drift), makesgenerate --lockedfail on drift, and makes the skills server refuse a served skill that the lock does not pin or whose digest differs. A corrupt lock, a lock of anotherversionor a source that cannot be snapshotted is anAR981finding, not a logged skip.generate --checkverifies authored content against an enforced lock, like--locked.- The lock digest ignores the project-wide
Source-Hashheader, so editing an unrelated file no longer changes the digest of a served skill. - A served skill the security scan refuses no longer stops
lock. It is left unpinned,lockexits3, andlock --refuse-findingsrestores the fail-without-writing behavior. lockandupdatescan what they pin. Every remote tree pinned to something new is scanned (AR001-AR009) first; an error finding refuses the pin (exit2, nothing written) unless--accept-findings.generate --checkclassifies machine-local inputs likegenerate. With aconfig.local.*overlay orlocal/content it no longer reports every output asstale. A shared file the local input would change, and thatgeneraterefuses to write (tracked or not ignored), is reported asblocked: <path>with exit2;--allow-local-driftaccepts it.
Exit codes¶
All commands follow one contract: 0 success, 1 the command could not run (invalid configuration, missing input,
tool error), 2 findings, drift or a failed gate, 3 only for lock. Scripts that matched 1 for a drift result
need to match 2.
| Command | 0 |
1 |
2 |
3 |
|---|---|---|---|---|
generate |
Written | Failed to load, validate or generate (any root with --recursive) |
--check found drift; --locked/--frozen source differs from the lock; recursive run where every failure is lock drift |
|
validate |
Valid | Invalid configuration | --strict: findings at or above --fail-on |
|
scan |
Clean | Cannot run | Findings at or above --fail-on |
|
verify |
Verified | Cannot run (no mode given, no trusted signer, no trusted root) | A signature, approval or plugin bundle failed verification | |
lock |
Written or verified | Cannot run; --check with no ai-rulez.lock; unknown name |
--check found drift (also a lock without content pins, or no lock under enforce); --outdated moved tag or unsatisfiable constraint; --fail-on-outdated |
Lock written, served skills left unpinned by the scan |
update |
Done or nothing to do | Cannot run | A source was refused (AR730, AR731, AR732); nothing written |
|
doctor |
No errors | Configuration does not load | An error (or, with --strict, a warning) |
|
verifiers run |
None failed | Nothing failed but the run could not complete | A verifier failed at --fail-on |
|
eval run |
All pass | Flags invalid | A skill failed its threshold, errored or has invalid cases | |
tokens, cost |
Within budget | Configuration cannot load | Over --budget or --on-demand-budget |
|
convert |
Done | Cannot run, or would overwrite files | Blocked by the scan or validation, or --fail-on matched |
|
export okf, import okf, okf validate |
Done | Cannot run | Drift (--check), lint findings, files not overwritten, or refused by the scan |
|
search --eval |
Pass | Cannot run (AR9D2) |
A gate failed (AR9D4) |
|
scanners doctor |
Healthy | Configuration does not load or a name is unknown | A checked scanner is missing or misconfigured | |
guard (hook) |
Allowed | The call edits a generated file |
Unknown subcommands (telemetry bogus) exit 1. When a command covers several roots
(--recursive, for generate --check and lock) the most severe code wins: 1, then 2, then 3.
Supply-chain defaults¶
- Plain
http://andgit://remotes are rejected.git://is unauthenticated and can be rewritten in transit. A remote include, OKF include, installed skill or skill source must usehttps://,ssh:///git@host:path, or a localfile://URL or path. The error names the source and says to switch tohttps://;include addrefuses them too. Include sources accept a leadinggit+(git+https://host/org/repo). - The git token goes only to allowlisted hosts.
AI_RULEZ_GIT_TOKEN/--tokenis sent togithub.comby default, or to the hosts inAI_RULEZ_GIT_TOKEN_HOSTS(comma separated, environment only; when set it replaces the default, so listgithub.comtoo), as a host-scoped header rather than inside the URL, and only overhttps://. Set the variable to keep using a token with GitLab, Bitbucket or a self-hosted host; other hosts get no token and a warning. Caches written by earlier versions are scrubbed. - The forge token has its own allowlist. The forge client (release dates for
min_release_age, review-linked approvals) sends the GitHub token (GITHUB_TOKEN,GH_TOKENorgh auth token) only togithub.comor the hosts inAI_RULEZ_FORGE_HOSTS(comma separated, environment only), never to everyAI_RULEZ_GIT_TOKEN_HOSTShost. For GitHub Enterprise Server, add its host toAI_RULEZ_FORGE_HOSTS. See Forge client. - A committed config cannot point outside the project. A local include (
sourceorlocal_override) that resolves outside the project after symlinks (../victim, an absolute path) is a fatal error. It is still allowed inconfig.local.tomland the user config. A local[[skill_sources]]urlorpathmust resolve inside the project (mcp --source <dir>and the user config may point anywhere).local_overrideon an include or installed skill in the committed config is refused undergenerate --locked,--frozenand an enforced lock, because it bypasses the pins; set it inconfig.local.toml. - Imported content is scanned by default.
[lint.security] scan_importsis on when unset:generatescans includes and installed skills before writing anything and stops at an error-level finding. Skills from includes (and any skill file outside the project) are scanned at the strict level. Setscan_imports = "off"to opt out, or"warn"to log only. - Unpinned MCP packages (
AR012) stay a warning invalidate, and are an error whenever[lock] enforceis on (wheneverai-rulez.lockexists, unlessenforce = false), together withAR010. - Content symlinks follow one policy. In the project's own
.ai-rulez/(including domains, skill and command resources), a symlinked file or directory is followed only when its fully resolved target is inside the repository root (the git top level, else the directory holding.ai-rulez); the refusal names that root. An enclosing repository widens that root only when it tracks the project (its index holds.ai-rulez/config.toml): a project in a monorepo may link to its siblings, but a project that merely sits below a$HOMEdotfiles repository is held to its own directory.GIT_CEILING_DIRECTORIESentries are resolved through symlinks, as git does. A project not yet added to its repository has its own directory as the root untilgit add. A symlinkedconfig.tomlorconfig.local.tomlfollows the same boundary: a target outside the root is a load error. Any other link used to be dropped silently; it is now refused with a warning that is shown even with--quiet, andai-rulez validatereports it as an error. Symlinks in includes (git or local), installed skills, skill sources and OKF bundles are never followed and are skipped with a warning (an installed skill with a symlinkedSKILL.mdis refused).init --fromnever follows symlinks. Repository content read at load time is capped at 8 MiB per file; a larger file is an error. scanis security-only.ai-rulez scanruns only thesecurityanalyzer's checks. Hook and config findings (AR504,AR9K0, ...) that used to appear in its report belong tovalidate --strict.
Scanner isolation¶
Staged scanners ([[lint.external]] with inputs) run confined under isolation = "auto", the default, wherever a
backend works: macOS sandbox-exec, Linux bwrap or unshare. A confined scanner has no network (unless it declares
egress = true) and cannot write outside its scratch directory, so one that writes elsewhere now fails with AR9E3.
Point it at TMPDIR/HOME, or set isolation = "none" on the entry or in [lint.scanner_policy]. Without a backend,
auto runs unconfined and notes AR9E7; isolation = "require" refuses to run instead. See
Isolation.
Organization policy¶
v5 reads a tighten-only organization policy from outside the repository: --policy, AI_RULEZ_POLICY, or the
managed path (/etc/ai-rulez/policy.toml, /Library/Application Support/ai-rulez/policy.toml,
%ProgramData%\ai-rulez\policy.toml). Nothing changes without one. When one applies, a repository value that
loosens it is clamped and reported (AR740), and generate and validate refuse such a configuration unless
--policy-mode warn. See Organization policy.
Approvals¶
Approvals (ai-rulez approve, [governance]) are new in v5. Pre-release v5 builds counted some approvals that no
longer count:
- Review-linked approvals need the pull request's final head.
approve --from-github-reviewrecords a review only when it was made on the head the pull request ends with; a review of an earlier push does not count. Re-run it after new pushes. - Outsider approvals do not count. A review counts only when the reviewer's
author_associationisOWNER,MEMBERorCOLLABORATOR, orapproversor CODEOWNERS name them, so a drive-by approval on a public repository is ignored. The digest is recomputed from the files at the reviewed commit, not taken from the lock committed there. [governance] max_ageis a ceiling. An approval stops counting (AR712) onceapproved_atplusmax_agehas passed, whatever itsexpiressays, andapprove --expiresbeyond it is refused.
See Approvals.
Go API¶
Packages under internal/ are not a public API, and v5 changed several of them. Code that embeds ai-rulez should use
github.com/Goldziher/ai-rulez/v5/pkg/airulez (experimental, see Embedding):
- The process-wide preset registry is gone:
config.GetPresetGenerator,config.PresetRegistry,config.RegisterPresetandconfig.RegisterRulesDirare removed; each generation carries its own registry. config.LoadConfigfails when the config declares includes or installed skills unless the load is givenconfig.WithResolvers(orconfig.WithoutRemote).config.SetPolicyEnforceris gone: the organization policy belongs to the load (config.WithPolicy).- The V2/V3 helpers (
config.DetectConfigVersion,config.VersionDir,config.ConfigVersionV3,Config.IsV3,config.DecodeLegacyMCPFile,config.MigrateLocalOverlayToTOML,LocalOverlay.Format) are removed. - In
pkg/airulez, the machine-local overlay (config.local.toml,.ai-rulez/local/) is read only withOptions.WithLocal;Options.WithoutLocalis removed.
Trust rule for [llm] and [telemetry]¶
allow_network, base_url, api_key_env and the price overrides of [llm], and the egress keys of [telemetry]
(allow_network, otlp_endpoint, headers_env, service_name, resource, ...), are honoured only from the user
config file (~/.config/ai-rulez/config.toml) or the AI_RULEZ_LLM_* / AI_RULEZ_TELEMETRY_* variables. A
repository config.toml or config.local.* that sets them is ignored and reported by llm doctor,
telemetry doctor, ai-rulez doctor and the strict findings AR9L1 and AR9K1. A literal key in either table is
AR9L0 or AR9K0. If a repository relied on setting these, move them to the user config file. [telemetry]
service_name is the v5 addition to this list: it labels data on the user's collector, so a repository value is
ignored. See LLM access and Telemetry.
The same rule now covers every egress or execution knob (scanner egress, eval execution, git credentials, hooks) and is documented once, with a table of each knob's scope and precedence, in the trust model.
Hooks, validation and the rule registry¶
validate --stricthas more rules. About thirty new codes (AR304,AR305,AR403,AR504-AR507,AR601,AR602,AR805-AR807,AR963,AR964,AR971-AR982,AR9A0-AR9B9,AR9C*,AR9K*,AR9L*and more) can fail a CI job that passed before. Every code is listed in Strict validation; lower or turn off a rule with[lint.severity], or accept current findings withvalidate --strict --update-baseline. Rule codes are stable and are never renumbered.[lint.budget]is renamed[lint.tolerate](tolerated findings per rule), so it no longer reads like[lint.budgets.<kind>](size budgets).[lint.budget]still works and warns. The report saysover its tolerated count of N; the JSON keybudgets_exceededis unchanged.- Top-level
[[hooks]]validation is stricter: ascriptoutsideA-Za-z0-9._/-is rejected, a missing or non-executable script isAR504/AR505, and every generated shell line quotes the path. - Served skills (
delivery = "served") are left out of the harness skill trees and reach the model throughai-rulez mcp --serve-skills; a static reference to one isAR990. Skills default tostatic, so nothing changes until you opt in. - Eval results are signed per user.
eval runsigns each stored record with a per-user key (eval-results.key, in the user config directory). A record without a valid signature, such as one committed from another machine, isunverified:eval runre-runs it, andAR997,AR998,report evalsandreport usageignore it. CI has no key; gate oneval run --force. See Evals. - Usage log version 3. The
sessionfield is a salted hash of the harness session id, not the raw id, and lines carrydigest,digest_schemeandevent_id(the lock's skill digest). Version 1 and 2 logs still read. --jsonis replaced by--format json. Every command that printed JSON takes--format text|json(some addsarif,junit,markdown);--jsonstays as a hidden alias that warns. An unknown--formatvalue is rejected with the allowed values.lock --formatis accepted only with--check,--diff,--outdatedor--subject.schema/catalog.schema.jsonis catalog version 2. The version 1 schema moved toschema/catalog.v1.schema.json;catalog --format jsonstill prints version 1 unless--schema-version 2.
Where to look¶
- Supported harnesses: the preset list and what each writes.
- Hooks, permissions and settings keys, Permissions and
User-level configuration: top-level
[[hooks]],[permissions]andgenerate --user. - Lock file and Trust model: the lock scheme and what a repository may set.
- Changelog: the complete list for 5.0.0.